Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I prevent a variadic constructor from being preferred to the copy constructor?

I have a template 'Foo', which owns a T, and I'd like it to have a variadic constructor that forwards its arguments to T's constructor:

template<typename T>
struct Foo {

    Foo()
        : t() {}

    Foo(const Foo& other)
        : t(other.t) {}

    template<typename ...Args>
    Foo(Args&&... args)
        : t(std::forward<Args>(args)...) {}

    T t;
};

However, this causes Foo to not be copyable:

int main(int argc, char* argv[]) {
    Foo<std::shared_ptr<int>> x(new int(42));
    decltype(x) copy_of_x(x);  // FAILS TO COMPILE
    return EXIT_SUCCESS;
}

because, according to this answer, the non-constness of the argument causes the variadic constructor to be a better match. I don't want to force my callers to use const_cast, for obvious reasons.

One possible solution I found was to write a "copy constructor" for Foo which takes a non-const Foo and uses constructor forwarding:

Foo(Foo& other)
    : Foo(const_cast<const Foo&>(other)) {}

When this constructor is defined, things work again: now the non-const Foo argument copy ctor is preferred. However, this seems very sketchy to me, in that this "cure" seems worse than the disease.

Is there another way to achieve this effect, to indicate that the natural copy constructor should be preferred to the variadic constructor? If not, are there any adverse consequences of defining this non-const argument copy constructor?

like image 435
acm Avatar asked Dec 18 '12 16:12

acm


4 Answers

You can use some ugly SFINAE with std::enable_if, but I'm not sure it is better than your initial solution (in fact, I'm pretty sure it's worse!):

#include <memory>
#include <type_traits>

// helper that was not included in C++11
template<bool B, typename T = void> using disable_if = std::enable_if<!B, T>;

template<typename T>
struct Foo {

    Foo() = default;
    Foo(const Foo &) = default;

    template<typename Arg, typename ...Args, typename = typename
        disable_if<
            sizeof...(Args) == 0 &&
            std::is_same<typename
                std::remove_reference<Arg>::type,
                Foo
            >::value
        >::type
    >
    Foo(Arg&& arg, Args&&... args)
        : t(std::forward<Arg>(arg), std::forward<Args>(args)...) {}

    T t;
};

int main(int argc, char* argv[]) {
    Foo<std::shared_ptr<int>> x(new int(42));
    decltype(x) copy_of_x(x);
    decltype(x) copy_of_temp(Foo<std::shared_ptr<int>>(new int));
    return 0;
}
like image 61
Luc Touraille Avatar answered Nov 17 '22 19:11

Luc Touraille


The best approach is to not do what you're doing.

That said, a simple fix is to let the variadic constructor forward up to a base class constructor, with some special first argument.

E.g. the following compiles with MinGW g++ 4.7.1:

#include <iostream>         // std::wcout, std::endl
#include <memory>           // std::shared_ptr
#include <stdlib.h>         // EXIT_SUCCESS
#include <tuple>
#include <utility>          // std::forward

void say( char const* const s ) { std::wcout << s << std::endl; }

template<typename T>
struct Foo;

namespace detail {
    template<typename T>
    struct Foo_Base
    {
        enum Variadic { variadic };

        Foo_Base()
            : t()
        { say( "default-init" ); }

        Foo_Base( Foo_Base const& other )
            : t( other.t )
        { say( "copy-init" ); }

        template<typename ...Args>
        Foo_Base( Variadic, Args&&... args )
            : t( std::forward<Args>(args)... )
        { say( "variadic-init" ); }

        T t;
    };

    template<typename T>
    struct Foo_ConstructorDispatch
        : public Foo_Base<T>
    {
        Foo_ConstructorDispatch()
            : Foo_Base<T>()
        {}

        template<typename ...Args>
        Foo_ConstructorDispatch( std::tuple<Foo<T>&>*, Args&&... args )
            : Foo_Base<T>( args... )
        {}

        template<typename ...Args>
        Foo_ConstructorDispatch( std::tuple<Foo<T> const&>*, Args&&... args )
            : Foo_Base<T>( args... )
        {}

        template<typename ...Args>
        Foo_ConstructorDispatch( void*, Args&&... args)
            : Foo_Base<T>( Foo_Base<T>::variadic, std::forward<Args>(args)... )
        {}
    };
}  // namespace detail

template<typename T>
struct Foo
    : public detail::Foo_ConstructorDispatch<T>
{
    template<typename ...Args>
    Foo( Args&&... args)
        : detail::Foo_ConstructorDispatch<T>(
            (std::tuple<Args...>*)0,
            std::forward<Args>(args)...
            )
    {}
};

int main()
{
    Foo<std::shared_ptr<int>>   x( new int( 42 ) );
    decltype(x)                 copy_of_x( x );
}
like image 30
Cheers and hth. - Alf Avatar answered Nov 17 '22 19:11

Cheers and hth. - Alf


If not, are there any adverse consequences of defining this non-const argument copy constructor?

I am going to ignore the "If not", since there are other approaches. But there is an adverse consequence of your approach. The following still uses the template constructor

Foo<X> g();
Foo<X> f(g());

Because g() is an rvalue, the template is a better match because it deduces the parameter to an rvalue reference.

like image 2
Johannes Schaub - litb Avatar answered Nov 17 '22 19:11

Johannes Schaub - litb


Disable the constructor when the argument type is the same type as or derived from this:

template<typename ThisType, typename ... Args>
struct is_this_or_derived : public std::false_type {};

template<typename ThisType, typename T>
struct is_this_or_derived<ThisType, T>
    : public std::is_base_of<std::decay_t<ThisType>, std::decay_t<T> >::type {};

template<typename ThisType, typename ... Args>
using disable_for_this_and_derived 
      = std::enable_if_t<!is_this_or_derived<ThisType, Args ...>::value>;

Use it as

template<typename ...Args
        , typename = disable_for_this_and_derived<Foo, Args ...> >
                                                //^^^^
                                                //this needs to be adjusted for each class
Foo(Args&&... args) : t(std::forward<Args>(args)...) {}
like image 1
davidhigh Avatar answered Nov 17 '22 19:11

davidhigh