21

I would like to enforce the type of variadic template to be identical to an earlier set template type. In the below example, I'd like T and U to be the same type.

code on ideone.com

#include <iostream>
#include <string>

template<class T>
struct Foo {

    Foo(T val) {
        std::cout << "Called single argument ctor" << std::endl;
        // [...]    
    }    

    // How to enforce U to be the same type as T?
    template<class... U>
    Foo(T first, U... vals) {
        std::cout << "Called multiple argument ctor" << std::endl;
        // [...]   
    }

};

int main() {

    // Should work as expected.
    Foo<int> single(1);

    // Should work as expected.
    Foo<int> multiple(1, 2, 3, 4, 5);

    // Should't work (but works right now). The strings are not integers.
    Foo<int> mixedtype(1, "a", "b", "c");

    // Also shouldn't work. (doesn't work right now, so that is good)
    Foo<int> alsomixedtype(1, 1, "b", "c");
}
Gerard
  • 831
  • 6
  • 15
  • Related: [A variadic template method to accept a given number of doubles?](http://stackoverflow.com/questions/30179181) – Ruslan Jun 25 '15 at 15:03
  • For a `concept` based solution, see: https://stackoverflow.com/a/61483494/2085626 – Amir Kirsh Apr 28 '20 at 15:07

5 Answers5

12

We can use SFINAE to ensure that all U types are the same as T. An important thing to note is that U is not just one type as you imply, but a list of possibly disparate types.

template<class... U, std::enable_if_t<all_same<T, U...>::value>* = nullptr>
Foo(T first, U... vals) {
    std::cout << "Called multiple argument ctor" << std::endl;
    // [...]   
}

std::enable_if_t is from C++14. If that's not an option for you, just use std::enable_if.

typename std::enable_if<all_same<T, U...>::value>::type* = nullptr>

all_same can be implemented in a bunch of different ways. Here's a method I like using boolean packs:

namespace detail
{
    template<bool...> struct bool_pack;
    template<bool... bs>
    //if any are false, they'll be shifted in the second version, so types won't match
    using all_true = std::is_same<bool_pack<bs..., true>, bool_pack<true, bs...>>;
}
template <typename... Ts>
using all_true = detail::all_true<Ts::value...>;

template <typename T, typename... Ts>
using all_same = all_true<std::is_same<T,Ts>...>;
TartanLlama
  • 63,752
  • 13
  • 157
  • 193
  • 2
    You don't have to implement `all_same` in C++17 anymore, as it has `std::conjunction` https://stackoverflow.com/a/50939173/8414561 – Dev Null Jun 20 '18 at 02:56
6

std::conjunction (logical AND) was introduced in C++17 so one doesn't have to implement all_same manually anymore. Then the constructor becomes simply:

template<typename... U,
    typename = std::enable_if_t<
        std::conjunction_v<
            std::is_same<T, U>...
        >
    >
>
Foo(T first, U... vals)
{
    std::cout << "Called multiple argument ctor" << std::endl;
    // [...]   
}

See live example.

Dev Null
  • 4,731
  • 1
  • 30
  • 46
4

C++20 concepts make it as simple as

    template<std::same_as<T>... U>
    Foo(T first, U... vals) {
        std::cout << "Called multiple argument ctor" << std::endl;
        // [...]   
    }

https://gcc.godbolt.org/z/neEsvo

Dvir Yitzchaki
  • 487
  • 4
  • 13
0

If all parameters are required to be the same type AND the number of parameters is variable,

Variadic templates are too heavy for those requirements, just use C++11 std::initializer_list.

They just do the job if you can replace () with {} on the call.

template<class T> struct Foo {
    Foo(T val) {
        std::cout << "Called single argument ctor" << std::endl;
    }
    // Enforce all parameters to be the same type :
    Foo( std::initializer_list<T> values ) {
        std::cout << "Called multiple argument ctor" << std::endl;
        for (T value : values)
            cout << value << endl;
    }
};

int main() {
    // Work as expected.
    Foo<int> single(1);
    // Work as expected.
    Foo<int> multiple{ 1, 2, 3, 4, 5 };
    // Doesn't work - as required :
    //Foo<int> mixedtype{ 1, "a", "b", "c" };

}
Emmanuel DURIN
  • 4,803
  • 2
  • 28
  • 53
  • `std::initializer_list` can't be used with move-only types (imagine `std::vector>`), however they can be handled with a parameter pack as in the question as formulated. – Dev Null Jun 20 '18 at 02:54
-1

Without implementing of all_same you could also change the constructor code as follows:

template<class F, typename = typename enable_if<is_same<F, T>::value>::type, class... U>
    Foo(F first, U... vals): Foo(vals...) {
    std::cout << "Called multiple argument ctor" << std::endl;
    // [...]   
}

is_same is the function in an STL <type_traits>

W.F.
  • 13,888
  • 2
  • 34
  • 81
  • hard-coding the type `int` restricts the use of this function, – Jagannath May 20 '15 at 10:53
  • @Jagannath actually it can be whatever also e.g. void as it is only to determine if the type is valid – W.F. May 20 '15 at 10:54
  • @Jagannath the key thing is we call constructor of inner type constructor recursively until we get only one parameter – W.F. May 20 '15 at 10:57
  • 1
    Yeah, the `int` is fine, but you don't need to specify anything as `void` is used as the default argument, so it just muddies the declaration. – TartanLlama May 20 '15 at 11:00
  • Indeed this works with the use-cases; however, I'm not not too keen on the recursion, as it would require reworking existing logic, i.e., I would need to detect when the last recursive call is made. – Gerard May 20 '15 at 11:00
  • The last call will be made for the last parameter. @TartanLlama - edited – W.F. May 20 '15 at 11:02
  • @Wojciech each successive constructor call could override the results of the previous call - I'd like to not do that, let's say, for performance reasons. – Gerard May 20 '15 at 11:08
  • @W.F. how can you use `first`-s in the final constructor? – Dev Null Jun 15 '18 at 00:54
  • @DevNull you can always create an overloaded constructor for a single deduced parameter... – W.F. Jun 15 '18 at 10:16
  • @W.F. I don't understand how you can use _all the parameters_ in a single place (say, last constructor). As a possibility - create an `std::vector`, initialise it inside the last constructor and `push_back` in the body of every single constructor that split out a `first`? – Dev Null Jun 20 '18 at 03:00