c++ variant class member stored by reference

2.4k views Asked by At

I am trying to experiment with std::variant. I am storing an std::variant as a member of a class. In the below code, things work fine if the variant is stored by value, but does not work (for the vector case, and for custom objects too) if the variant is stored by reference. Why is that?

#include <variant>
#include <vector>
#include <iostream>


template<typename T>
using VectorOrSimple = std::variant<T, std::vector<T>>;


struct Print {
void operator()(int v) { std::cout << "type = int, value = " << v << "\n"; }
void operator()(std::vector<int> v) const { std::cout << "type = vector<int>, size = " << v.size() << "\n"; }
};


class A {
public:
    explicit A(const VectorOrSimple<int>& arg) : member(arg) {
        print();
    }

    inline void print() const {
        visit(Print{}, member);
    }

private:
    const VectorOrSimple<int> member;  // const VectorOrSimple<int>& member; => does not work
};


int main() {
    int simple = 1;
    A a1(simple);
    a1.print();

    std::vector<int> vector(3, 1);
    A a2(vector);
    a2.print();
}

See http://melpon.org/wandbox/permlink/vhnkAnZhqgoYxU1H for a working version, and http://melpon.org/wandbox/permlink/T5RCx0ImTLi4gk5e for a crashing version with error : "terminate called after throwing an instance of 'std::bad_variant_access' what(): Unexpected index"

Strangely, when writing a boost::variant version of the code with the member stored as a reference, it works as expected (prints vector size = 3 twice) with gcc7.0 (see here http://melpon.org/wandbox/permlink/eW3Bs1InG383vp6M) and does not work (prints vector size = 3 in constructor and then vector size = 0 on the subsequent print() call, but no crash) with clang 4.0 (see here http://melpon.org/wandbox/permlink/2GRf2y8RproD7XDM).

This is quite confusing. Can someone explain what is going on? Thanks.

2

There are 2 answers

0
StoryTeller - Unslander Monica On BEST ANSWER

It doesn't work because this statement A a1(simple); creates a temporary variant object!

You then proceed to bind said temporary to your const reference. But the temporary goes out of scope immediately after the construction of a1 is over, leaving you with a dangling reference. Creating a copy works, obviously, since it always involves working with a valid copy.

A possible solution (if the performance of always copying worries you) is to accept a variant object by-value, and then move it into your local copy, like so:

explicit A(VectorOrSimple<int> arg) : member(std::move(arg)) {
    print();
}

This will allow your constructor to be called with either lvalues or rvalues. For lvalues your member will be initialized by moving a copy of the source variant, and for rvalues the contents of the source will just be moved (at most) twice.

0
Yakk - Adam Nevraumont On

Variants are objects. They contain one of a set of types, but they are not one of those types.

A reference to a variant is a reference to the variant object, not a reference to one of the contained types.

A variant of reference wrappers may be what you want:

template<class...Ts>
using variant_ref=std::variant<std::reference_wrapper<Ts>...>;

template<typename T>
using VectorOrSimple = std::variant<T, std::vector<T>>;
template<typename T>
using VectorOrSimpleRef = variant_ref<T, std::vector<T>>;
template<typename T>
using VectorOrSimpleConstRef = variant_ref<const T, const std::vector<T>>;

Now store VectorOfSimpleConstRef<int>. (Not const&). And take one in the constructor as well.

Also modify Print to take by const& to avoid needlessly copying that std::vector when printing.