Search code examples
c++c++17std-variant

Function accepting a reference to std::variant


I'm trying to pass values to a function accepting a std::variant. I noticed I can use a function accepting a const reference to a variant value, but not a reference alone. Consider this code

#include <variant>
#include <queue>
#include <iostream>

struct Foo{ std::string msg{"foo"}; };
struct Bar{ std::string msg{"bar"}; };

using FooBar = std::variant<Foo,Bar>;

void f1(const FooBar&)
{
    std::cout << "yay" << std::endl;
}

void f2(FooBar&)
{
    std::cout << "wow" << std::endl;
}

int main()
{
    Foo f;
    Bar b;
 
    f1(f); // fine
    f1(b); // fine 
    f2(f); // compile error
}

gives me error

invalid initialization of reference of type 'FooBar&' {aka 'std::variant<Foo, Bar>&'} from expression of type 'Foo'
   42 |     f2(f);

so the first question is: why is that prohibited? I can't figure out.

Why I'm doing this? I'm trying to use two accessor function to read and modify the values using std::visit, something like this:

#include <variant>
#include <queue>
#include <iostream>

struct Foo{ std::string msg{"foo"}; };
struct Bar{ std::string msg{"bar"}; };

using FooBar = std::variant<Foo,Bar>;
   
std::string f3(const FooBar& fb)
{
    return std::visit([](auto& foobar){
        std::string ret = "yay ";
        return ret + foobar.msg;
    }, fb);
}

void f4(FooBar& fb)
{
    std::visit([](auto& foobar){
        foobar.msg += "doo";
    }, fb);
}

int main()
{
    Foo f;
    Bar b;

    std:: cout << f3(f) << " " << f3(b); // fine
    f4(f); // does not compile
}

which of course does not compile with

error: cannot bind non-const lvalue reference of type 'FooBar&' {aka 'std::variant<Foo, Bar>&'} to an rvalue of type 'FooBar' {aka 'std::variant<Foo, Bar>'}
   44 |     f4(f);
      |        ^

So second question: how can I achieve this behaviour?


Solution

  • You cannot pass a temporary resulting from converting the Foo to a std::variant<Foo,Bar> to f2. C++ disallows this because binding a temporary to a non-const reference is most likely a bug. If it was possible you'd have no way to inspect the modified value anyhow.

    You can workaround this by using a std::variant of references (actually std::reference_wrapper) and pass that by value. You'd then use two overloads, one for non-const and one for const references, in both cases the temporary std::variant can be passed by value while modifications inside the function are made directly on the object used to construct the std::variant:

    #include <variant>
    #include <queue>
    #include <iostream>
    #include <functional>
    
    struct Foo{ std::string msg{"foo"}; };
    struct Bar{ std::string msg{"bar"}; };
    
    using FooBar = std::variant<std::reference_wrapper<Foo>,std::reference_wrapper<Bar>>;
    using ConstFoobar = std::variant<std::reference_wrapper<const Foo>,std::reference_wrapper<const Bar>>;
    
    void f1(ConstFoobar)
    {
        std::cout << "yay" << std::endl;
    }
    
    void f2(FooBar)
    {
        std::cout << "wow" << std::endl;
    }
    
    int main()
    {
        Foo f;
        Bar b;
     
        f1(f); // yay
        f1(b); // yay 
        f2(f); // wow
    }
    

    Complete Example

    PS: The simpler solution is of course to have two overloads taking plain Foo and Bar (ie for each a non and a const reference overload, makes 4 in total). Though I suppose you are using std::variant for some other reasons not directly apparent from the example code.