Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

std::tuple with generic types like boost::any

Tags:

c++

c++11

Dear fellow programmers,

the code below gives me some headaches. It tries to add a 'generic' object (=object that can be constructed from anything) to a tuple and then copy that tuple.

    #include <tuple>
    #include <iostream>
    #include <typeinfo>


    struct anything
    {
       anything()
       {}

       anything(const anything&)
       {
          std::cout << "Called copy c'tor" << std::endl;
       }

       template<class T>
       anything(T arg)
       {
          std::cout << "Called c'tor with with argument of type " << typeid(arg).name() << std::endl;
          // new T(arg); // causes stack overflow
       }
    };


    int main()
    {
       std::tuple<anything> t;
       //
       std::cout << "Copy constructing t2, expecting copy c'tor to be called." << std::endl;
       std::tuple<anything> t2(t);

       return 0;
    }

With VS 2015 Update 2 it doesn't even compile, the line

std::tuple<anything> t2(t);

triggers a compiler error deep in tuple.h. With gcc 5.3.1 it compiles, but the output is not what I'd expect:

Copy constructing t2, expecting copy c'tor to be called.
Called copy c'tor
Called c'tor with with argument of type St5tupleIJ8anythingEE

What I don't understand is the last line, i.e. why the templated constructor gets called with std::tuple as argument?

This is actually a real world problem. In my application I use a boost::signal of signature

 typedef boost::type_erasure::any
         <boost::mpl::vector<
         boost::type_erasure::typeid_<>,
         boost::type_erasure::copy_constructible<>,
         boost::type_erasure::less_than_comparable<>,
         boost::type_erasure::equality_comparable<>,
         boost::type_erasure::relaxed
         >> KeyType;

boost::signals2::signal<void(const KeyType&)>

Boost signals internally wraps the argument in a tuple before calling the slot function with it, which eventually results in a stack overflow, because the tuple c'tor calls the any c'tor with tuple as argument and the any c'tor then calls the tuple c'tor with 'any of tuple' and so on and on and on...

like image 937
Florian Schmid Avatar asked Oct 18 '22 11:10

Florian Schmid


2 Answers

Let's go through overload resolution for:

std::tuple<anything> t2(t);

We have three viable constructors at our disposal:

explicit tuple( const Types&... args ); // (2) with Types = [anything]

template< class... UTypes >
explicit tuple( UTypes&&... args );     // (3) with UTypes = [std::tuple<anything>&]

tuple( const tuple& other ) = default;  // (8)

Which have these argument lists:

tuple(const anything& );             // (2)
tuple(std::tuple<anything>& );       // (3)
tuple(std::tuple<anything> const& ); // (8)

(2) involves a user-defined conversion whereas (3) and (8) are exact matches. When it comes to reference bindings:

Standard conversion sequence S1 is a better conversion sequence than standard conversion sequence S2 if [...] S1 and S2 are reference bindings (8.5.3), and the types to which the references refer are the same type except for top-level cv-qualifiers, and the type to which the reference initialized by S2 refers is more cv-qualified than the type to which the reference initialized by S1 refers.

So (3) is preferred - since it's less cv-qualified than (8). That constructor calls anything(std::tuple<anything> ).


As far as solutions, what you need is for (3) to not be considered in this case - we need to make it not a viable option (since (8) is already preferred to (2)). Currently, the easiest thing is just to make your constructor explicit:

template<class T>
explicit anything(T arg) { ... }

this works since (3) is specified in terms of is_convertible<>, which will return false on explicit conversions. However, that's currently considered a defect and will likely be changed in the future - since after all, we are explicitly constructing every aspect here so the explicit constructors should still be considered!

Once that happens, you're kind of out of luck as far as direct copy construction goes. You'd have to do something like disable your anything constructor for tuple? That seems... not great. But in that case, marking that constructor explicit would still work for copy-initialization:

std::tuple<anything> t2 = t;

which works now even without marking the anything constructor explicit, due to the same aforementioned defect.

like image 124
Barry Avatar answered Nov 18 '22 20:11

Barry


If you look at the implementation of the tuple you'll notice that

_Tuple_val<_This> _Myfirst; // the stored element
...
template<class _This2,
    class... _Rest2,
    class = typename _Tuple_enable<tuple<_This2, _Rest2...>, _Myt>::type>
    explicit tuple(_This2&& _This_arg, _Rest2&&... _Rest_arg)
    : _Mybase(_STD forward<_Rest2>(_Rest_arg)...),
        _Myfirst(_STD forward<_This2>(_This_arg))
    {   // construct from one or more moved elements
    }

The constructor of the tuple passes first argument to the the constructor of the first element of tuple. As variable t has type std::tuple<anything> compiler finds the best match for constructing anything with t - a template constructor.

To copy a tuple you simply need to write

std::tuple<anything> t2 = t;

UPD.

According to standard std::tuple provides following constructors:

template <class... UTypes>
explicit constexpr tuple(const Types&...);

template <class... UTypes>
constexpr tuple(const tuple<UTypes...>&);

And apparently your template constructor is a better match than copy constructor of the tuple.

like image 42
Teivaz Avatar answered Nov 18 '22 18:11

Teivaz