Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

c++ variadic template constructor and common constructors

Code like (c++14):

struct S { int a; int b; };

class C
{
  public:
    C(char const*, size_t) {} // 1
    C(S const&) {} // 2
    C(S const*) {} // 3
    template<typename ...T> C(T&& ...) {} // 4

 // C(S) {} // 5
 // C(S*) {} // 6
};

S s { 1, 2 };
C c1 { s }; // calls 4 and not 2
C c2 { "abc", 3 }; // calls 4 and not 1
C c3 { (char const*)"abc", (size_t)3 }; // calls 1 - ok
C c4 { s }; // calls 5 if uncommented
C c5 { &s }; // calls 6 if uncommented
S const s2 {};
C c6 { &s2 }; // calls 3

Simple constructor is called if it has exact the same signature as the passed parameter. Is there some trick to use common constructors as usual with a variadic template constructor, without copying classes, passed as parameters, and overloading constructors like:

C(S const*) {}
C(S*) {}

And without additional tags in constructors

like image 312
Роман Коптев Avatar asked Oct 05 '15 21:10

Роман Коптев


2 Answers

Create two tiers of constructor. Then tag dispatch.

template<template<class...>class Z, class T>
struct is_template:std::false_type{};
template<template<class...>class Z, class...Ts>
struct is_template<Z, Z<Ts...>>:std::true_type{};

struct foo {
private:
  template<class T> struct tag{ explicit tag(int) {} };
public:
  foo( tag<std::true_type>, const char*, size_t );
  template<class...Ts>
  foo( tag<std::false_type>, Ts&&...ts );

public:
  foo() = default; // or whatever
  template<class T0, class...Ts,
    std::enable_if_t<!is_template<tag, std::decay_t<T0>>{},int> =0>
  foo(T0&&t0, Ts&&...ts):
    foo( tag<typename std::is_constructible< foo, tag<std::true_type>, T0&&, Ts&&... >::type>{0}, std::forward<T0>(t0), std::forward<Ts>(ts)... )
  {}
};

The "preferred" ctors are prefixed with std::true_type, the "less preferred" ctors are prefixed with std::false_type.

This has the usual imperfections of perfect forwarding. If you take initializer lists, you'll want to have another "public" ctor that takes that explicitly, for example. And function name argument magical overloading won't work. NULL is an int. Etc.

You can imagine a version that, instead of having two tiers, has an arbitrary number. The is_constructible< ... > clause in the public facing ctor instead is replaced with some magic that finds the highest N such that tag<N>, blah... can construct the object (or, lowest N, whichever way you want to do it). Then it returns the type tag<N>, which then dispatches to that tier.

Using a technique like this:

template <typename... T,
      typename = std::enable_if_t<!std::is_constructible<C, T&&...>::value>
       >
C(T&&... ) { }

runs into a serious problem down the road, as we have instantiated is_constructible in a context where it gets the answer wrong. And in practice, compilers cache the results of template instantiations, so now the result of is_constructible is compiler order dependent (ODR violation I suspect).

like image 75
Yakk - Adam Nevraumont Avatar answered Nov 18 '22 16:11

Yakk - Adam Nevraumont


You can enable your variadic constructor if and only if those arguments do not allow you to construct C in some other way using std::is_constructible.

That is:

template <typename... T,
          typename = std::enable_if_t<!std::is_constructible<C, T&&...>::value>
           >
C(T&&... ) { }

With that change, C c1{s} does call (2) and C c2{"abc", 3} does call (1), whereas C c7{1, 2, 3, 4, 5} will call (4).

like image 32
Barry Avatar answered Nov 18 '22 17:11

Barry