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

Raymond Chen

Raymond

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.

In the case where Kenny used it, it was to permit the First method to return a different iterator depending on who’s asking for it. The underlying problem is the object wants to be able to produce a stream of T objects or IInspectable objects, so it implements both the IIterable<T>::First() and IIterable<IInspectable>::First() methods. The projection for those interfaces forward to the implementation’s First(), which forces First() to serve two masters. And the way he solved it was to return an ambiguous object, so that each master sees what it wants.

Raymond Chen
Raymond Chen

Follow Raymond   

10 comments

  • Avatar
    MNGoldenEagle

    That’s actually a really cool trick. I didn’t realize C++ had the ability to overload casting operators (which also sounds terrifying). Now I’m curious how this could be implemented in C# or Kotlin…

  • Avatar
    Kalle Niemitalo

    I thought there was a proposal for a C++ feature that one could use in struct result, to force auto entity = exp.get_entity(); to be eagerly converted to some other type, but I can’t find it now. WG21 P0936R0 and P0849R2 are somewhat related, though.

    edit: Found P0672R0 immediately after posting the comment, of course.

    • Avatar
      GL

      From the wiki page, it seems the feature of Voldemort types is that it is anonymous, not that it can respond according to the larger expression of which it is a part.

      The method demonstrated in this entry is the well-known temporary proxy idiom. It’s canonically used for simulating C# indexer in C++ operator[] — think of this as a superposition of T& and T const.

  • Motti Lanzkron
    Motti Lanzkron

    I asked this very question as a puzzle on stackoverflow eleven years ago (back when stackoverflow allowed this kind of stuff).

    As for the problem with auto, it has also been bothering me for years, I suggested to Herb Sutter that C++ add a way for a class to configure the way it’s deduced for auto. My idea was to have an operator auto (which you can = delete if you want to prevent the problem presented in this post). Unfortunately this doesn’t seem to have gained any traction (and in C++14 the syntax is already legal so something else should be used).

  • Farid Mehrabi
    Farid Mehrabi

    C++17 allows you to use std::variant:

    std::variant experiment::get_entity(){
    //blah blah
    return widget{};
    //blah blah
    return doodad{};
    };

    experiment ex;
    auto res {ex.get_entity()};
    if (res.holds_alternative()){
    auto& w{std::get()};
    //use w
    };
    if (res.holds_alternative()){
    auto& d{std::get()};
    //use d
    };

    • Avatar
      Tim Weis

      Indeed, C++17 introduced std::variant. But that only solves part of the problem, namely, the inability to overload on return type only. It does not address the other part: Having the callee return a type the caller expects. Instead, this implementation subscribes to a specific type when calling get_entity(). The comments reading “blah blah” hide a problem for which there is no solution: Dispatching to the concrete type based on expectations of the caller, without any information on the expectations of the caller.

Leave a comment