Specifying one type for all arguments passed to variadic function or variadic template function w/out using array, vector, structs, etc?

Viewed 29452

This question is about guaranteeing all arguments are of the same type while exhibiting a reject-early behavior with a clean compiler error, not a template-gibberish error

I'm creating a function (possibly member function, not that it matters... maybe it does?) that needs to accept an unknown number of arguments, but I want all of them to be the same type. I know I could pass in an array or vector, but I want to be able to accept the list of args directly without extra structure or even extra brackets. It doesn't look like variadic functions by themselves are typesafe, and I wasn't sure how to go about this w/ variadic template functions. Here's essentially what I'm aiming for (more than likely not correct code, and totally not for the purpose of getting lists of dragons, lol):

//typedef for dragon_list_t up here somewhere.

enum Maiden {
    Eunice
    , Beatrice
    , Una_Brow
    , Helga
    , Aida
};

dragon_list_t make_dragon_list(Maiden...) {
    //here be dragons
}

OR

template<Maiden... Maidens> dragon_list_t make_dragon_list(Maidens...) {
    //here be dragons
}

USAGE

dragon_list_t dragons_to_slay
    = make_dragon_list(Maiden.Eunice, Maiden.Helga, Maiden.Aida)
;

Tried a few things similar to the above already, no dice. Suggestions? Obvious oversights I may have made? I know it may not be a huge deal to do this instead:

dragon_list_t make_dragon_list(std::array<Maiden> maidens) {
    //here be dragons.
}
dragon_list_t dragons_to_slay
    = make_dragon_list({Maiden.Eunice, Maiden.Helga, Maiden.Aida})
;

but I'd much rather be able to do it the first way if possible.

15 Answers

A recent proposal, Homogeneous variadic functions, addresses this by making something like your first construct legal. Except of course to use the parameter pack you will have to name it. Also the exact syntax doesn't seem very concrete yet.

So, under the proposal this will actually be legal (you can see a similar construct in the paragraph "The template introducer" in the paper):

dragon_list_t make_dragon_list(Maiden... maidens) {
    //here be dragons
}

using c++17, you can write

template <class T, class... Ts, class = std::enable_if_t<(std::is_same_v<T, Ts> && ...)>
void fun(T x, Ts... xs)
{
}

The code below will work using C++20 Concepts. Compile with the -std=c++20 flag.

#include <concepts>
#include <vector>

enum Maiden {
  Eunice
  , Beatrice
  , Una_Brow
  , Helga
  , Aida
};

using dragon_list_t = std::vector<Maiden>;

dragon_list_t make_dragon_list(std::same_as<Maiden> auto... xs) {
  return {xs...};
}

int main(int argc, char *argv[])
{
  make_dragon_list(Helga,Aida,Aida);
  return 0;
}

This uses the std::same_as concept from the C++20 standard library. You can also use the std::convertible_to concept if you want more flexibility.

This is almost direct alternative to (Maiden...).

dragon_list_t make_dragon_list()
{
    return dragon_list_t();
}
template<typename ...T>
auto make_dragon_list(Maiden first, T &&... other) -> decltype(make_dragon_list(std::forward<T>(other)...))
{
    //here be dragons
    return {first, std::forward<T>(other)...};
}

but it still does not support the construction by multiple variables. If Mained was a class, that can be constructed from 2 variables, when

make_dragon_list({1,0}) 

will work, but

make_dragon_list({1,0}, {2,3}) 

won't compile.

This is why this proposal so important

C++20 concepts makes it as nice as

template<std::same_as<Maiden>... T>
dragon_list_t make_dragon_list(Maiden...) {
    //here be dragons
}

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

A more general way to do this, without extra assumptions (like they are one certain, specific type) is to use a modern C++20+, requires clause, which to me sounds like the next best thing to having this built into the language in a more intuitive way.

Ideally, you could use std::same_as<Ts...> (concept) or std::is_same<Ts...> (type-trait), but in actuality you cannot because the standard doesn't provide them as variadic for some reason. (I'm not sure why; it's kind of annoying.)

So we will implement our own and then maybe get around to writing a proposal for the standard, because they are fundamental. We'll call them all_same (concept) and are_same (type-trait).

Here's how to use them:

template<typename... Ts> requires all_same<Ts...>
someFunction(Ts... ts){}

Which is pure C++20 style, using the concept. Or, a mix of C++20 and C++17:

template<typename... Ts> requires are_same_v<Ts...>
someFunction(Ts... ts){}

If you're stuck in C++17, then you could use SFINAE:

template<typename... Ts, std::enable_if_t<std::are_same_v<Ts...>, void>>
someFunction(Ts... ts){} // I feel so dirty

Implementations

C++20 Concept version.

template<typename... Ts>
concept all_same =
    (std::same_as<typename std::tuple_element_t<0, std::tuple<Ts...>>, Ts> && ...);

C++17 type-trait version.

template<typename... Ts>
struct are_same : std::integral_constant<bool, (std::is_same_t<typename std::tuple_element_t<0, std::tuple<Ts...>>, Ts> && ...)>
{};

template<typename... Ts>
inline constexpr bool are_same_v = are_same<Ts...>::value;

The implementation of our variadic are_same type-trait is written in terms of the binary std::is_same type trait. This could be quite useful for pre-C++20 code, but then of course you won't be able to use the requires keyword for it. Still useful by using SFINAE (std::enable_if) in lieu of concepts and C++20, and for other metaprogramming tasks.

What both of these do is use the tuple template facilities--which are metaprogrammatic in nature--to extract information about the type of the first element of the variadic pack, and then compare that against all the other types in the pack to ensure they are identical. There is no runtime overhead. This is all compile time.

void testFunction()
{
    someFunction(4, 38, 58); // Fine.
    someFunction("hello", "world", "how are you?"); // Fine.
    someFunction("wtf", 28, 482.3); // Error!
}

The first two work because all arguments are the same type. The third one fails with a compiler error because they aren't.

Related