35

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?

Community
  • 1
  • 1
acm
  • 12,183
  • 5
  • 39
  • 68
  • 1
    possible duplicate of [Are variadic constructors supposed to hide the implicitly generated ones?](http://stackoverflow.com/questions/2953611/are-variadic-constructors-supposed-to-hide-the-implicitly-generated-ones) – Bo Persson Dec 18 '12 at 17:13
  • My initial guess is that the only two solutions are either the extra overload for non-const, or some enable_if logic which excludes that case. Personally, I would go with the addition of a non-const copy constructor. – Dave S Dec 18 '12 at 17:20
  • 1
    @BoPersson Not really a duplicate. I'd read that question and answer (and even linked to it), but my question is more about whether declaring the non-const arg copy ctor as a workaround has adverse consequences. – acm Dec 18 '12 at 17:51
  • Related usefulness: http://stackoverflow.com/a/13328507/1170277 – mavam Dec 19 '12 at 07:29

4 Answers4

16

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;
}
Luc Touraille
  • 79,925
  • 15
  • 92
  • 137
  • I'm going to need to ponder that one for a bit (typename = typename!), but I suspect your conclusion that this is worse is probably correct. – acm Dec 18 '12 at 17:55
  • 1
    @acm: C++11 allows defaulted template parameters for functions, C++03 did not. It is a convenient place to apply `enable_if`. – Cheers and hth. - Alf Dec 18 '12 at 18:23
  • 1
    @acm Augment the `enable_if` with a `sizeof...(Args) == 0` check possibly as well. Ie, if there is 1 or more element in `Args`, it is enabled, and it is also enabled if the first element is not `Foo`. – Yakk - Adam Nevraumont Dec 18 '12 at 18:53
  • @acm: Regarding `typename = typename ...`, it is "simply" an unnamed template parameter with a default value. See also this page on [the Lounge wiki](http://loungecpp.wikidot.com/tips-and-tricks:enable-if-for-c-11). – Luc Touraille Dec 18 '12 at 19:58
  • @Yakk: indeed, the code I gave will fail if there are more than one parameter and the first one is a `Foo`. Let met correct that. – Luc Touraille Dec 18 '12 at 20:00
  • C++17 allows writing this more elegantly: `std::enable_if_t<!std::is_callable_v(Args&&...)>>` (no need to separate `Arg&&` from `Args&&...`). We test whether an invented function with same "signature" as the copy constructor could be called with the arguments and, if so, suppress the variadic constructor. – ecatmur Mar 02 '17 at 10:04
2

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 );
}
Cheers and hth. - Alf
  • 142,714
  • 15
  • 209
  • 331
  • Thanks, that is an interesting approach. Unfortunately I think that in the case of the thing I am trying to build that the additional complexity is probably not helpful. I'll probably just remove the attempt to have the template constructor. While nice in certain ways, it clearly is more trouble than it is worth. – acm Dec 19 '12 at 21:32
  • @acm: The "not do what you're doing" advice means to not directly pass all the variadic constructor arguments on to the contained thingy. You can still easily handle the *real problem* this way, but with e.g. a first argument that only says, pass this on to the contained thingy, please. Essentially, offer the user directly the `Foo_Base` class shown above, with **explicit** constructor choice, instead of the `Foo` class above with **implicit** (automated) constructor choice (which you asked for). Explicit = good, implicit = bad. Sorry to spell things out, but maybe it was not clear! :-) – Cheers and hth. - Alf Dec 19 '12 at 22:59
  • There's one huge disadvantage to this interesting approach: None of default-ctor, copy-ctor and move-ctor can be trivial anymore... – Deduplicator Feb 11 '16 at 22:25
  • @Deduplicator: I fail to see how it's a disadvantage that the forwarding constructor isn't trivial. – Cheers and hth. - Alf Feb 11 '16 at 23:42
  • @Cheersandhth.-Alf Well, some containers and algorithms special-case that for better performance. – Deduplicator Feb 12 '16 at 00:35
2

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.

Johannes Schaub - litb
  • 496,577
  • 130
  • 894
  • 1,212
  • Sure. I'd actually removed the move ctor when reducing the test case to make an example since I didn't want to make things more complex. At least with clang, it does not seem to select the template when Foo(Foo&&) is available. – acm Dec 19 '12 at 21:30
1

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)...) {}
davidhigh
  • 14,652
  • 2
  • 44
  • 75
  • You might want to take a look at [`std::is_convertible`](http://en.cppreference.com/w/cpp/types/is_convertible). – Deduplicator Feb 11 '16 at 22:28
  • @Deduplicator: I actually thought about adding another solution using `is_convertible`. It's easier to transfer between classes, but obviously not as general... if `T` can be constructed from a `Foo`, the `Foo(Foo&)` copy is shaded again. – davidhigh Feb 11 '16 at 22:33