How can I have a C++ function that returns different types depending on what the caller wants?

Raymond

November 6th, 2019

Here’s something crazy: You have a function that has two different callers. One of them expects the function to return a widget. The other expects the function to return a doodad.

class widget; class doodad; class experiment { public: doodad get_entity(); widget get_entity(); };

This is not allowed in C++. The return type of a function cannot be overloaded.

But Kenny Kerr taught me how to fake it. What you do is return an object that doesn’t yet know whether it’s a widget or doodad.

class experiment { public: auto get_entity() { struct result { operator widget() { return experiment->get_entity_as_widget(); } operator doodad() { return experiment->get_entity_as_doodad(); } experiment* experiment; }; return result{ this }; } };

The thing that is returned is neither a widget nor a doodad, but observing it will trigger a collapse to one or the other.

widget w = exp.get_entity(); doodad p = exp.get_entity();

In the first call, the get_ entity() returns the private result type, and then immediately assigns it to a variable of type widget . This triggers the operator widget() conversion operator, which calls get_ entity_ as_ widget .

Similarly, the second call obtains the private result type and converts it to a doodad , which winds up calling get_ entity_ as_ doodad .

The wave function collapse could be triggered by anything that accepts a conversion.

move_widget(exp.get_entity()); // will call get_entity_as_widget signal_doodad(exp.get_entity()); // will call get_entity_as_doodad

If you take the return value of get_ entity and save it in an auto , then the wave function hasn’t collapsed yet. It’s still not sure which thing it is.

auto entity = exp.get_entity();

The thing doesn’t become a widget or doodad until you convert it.

move_widget(entity); // calls get_entity_as_widget

Note that the call to get_ entity_ as_ widget is delayed until the conversion occurs.

auto entity = exp.get_entity(); exp.replace_entity(); move_widget(entity); // calls get_entity_as_widget

Between calling get_ entity and converting the result to a widget, we changed the entity in the experiment. Not until the conversion occurs does the call to get_ entity_ as_ widget happen, at which point it will get the new entity rather than the original one. And of course, if you destroy the experiment, then the unresolved entity has a dangling pointer, and the behavior is undefined.

This trick works best if the caller will convert the result immediately to its final type (widget or doodad).

Of course, you could try to fix these problems, say by taking a strong reference to the experiment to prevent it from being destructed prematurely. Or you could call both get_ entity_ as_ widget and get_ entity_ as_ doodad as part of the constructor, and then hand out the appropriate type during the conversion. That would fix the “delayed evaluation” problem, but at a cost of doing eager evaluation of both branches, even if only one will end up being used.