26

Since C++11 we can make template functions which can accept any sequence of arguments:

template <typename... Ts>
void func(Ts &&... ts) {
   step_one(std::forward<Ts>(ts)...);
   step_two(std::forward<Ts>(ts)...);
}

However, suppose that it really only makes sense to call my function in the case where each argument has the same type -- any number of arguments would be okay though.

What's the best way to do that, i.e. is there a good way to constrain the templates to make a nice error message in that case, or ideally, eliminate func from participating in overload resolution when the arguments don't match?


I can make it really concrete if it helps:

Suppose I have some struct:

struct my_struct {
  int foo;
  double bar;
  std::string baz;
};

Now, I want to be able to do things like, print the members of the struct for debugging purposes, serialize and deserialize the struct, visit the members of the struct in sequence, etc. I have some code to help with that:

template <typename V>
void apply_visitor(V && v, my_struct & s) {
  std::forward<V>(v)("foo", s.foo);
  std::forward<V>(v)("bar", s.bar);
  std::forward<V>(v)("baz", s.baz);
}

template <typename V>
void apply_visitor(V && v, const my_struct & s) {
  std::forward<V>(v)("foo", s.foo);
  std::forward<V>(v)("bar", s.bar);
  std::forward<V>(v)("baz", s.baz);
}

template <typename V>
void apply_visitor(V && v, my_struct && s) {
  std::forward<V>(v)("foo", std::move(s).foo);
  std::forward<V>(v)("bar", std::move(s).bar);
  std::forward<V>(v)("baz", std::move(s).baz);
}

(It looks a bit laborious to generate code like this, but I made a small library some time ago to help with that.)

So, now I would like to extend it so that it can visit two instances of my_struct at the same time. The use of that is, what if I want to implement equality or comparison operations. In boost::variant documentation they call that "binary visitation" as contrasted with "unary visitation".

Probably, no one will want to do more than binary visitation. But suppose I want to do like, general n-ary visitation. Then, it looks like this I guess

template <typename V, typename ... Ss>
void apply_visitor(V && v, Ss && ... ss) {
  std::forward<V>(v)("foo", (std::forward<Ss>(ss).foo)...);
  std::forward<V>(v)("bar", (std::forward<Ss>(ss).bar)...);
  std::forward<V>(v)("baz", (std::forward<Ss>(ss).baz)...);
}

But now, it's getting a little more squirrelly -- if someone passes a series of types that aren't even the same structure type at all, the code may still compile and do something totally unexpected by the user.

I thought about doing it like this:

template <typename V, typename ... Ss>
void apply_visitor(V && v, Ss && ... ss) {
  auto foo_ptr = &my_struct::foo;
  std::forward<V>(v)("foo", (std::forward<Ss>(ss).*foo_ptr)...);
  auto bar_ptr = &my_struct::bar;
  std::forward<V>(v)("bar", (std::forward<Ss>(ss).*bar_ptr)...);
  auto baz_ptr = &my_struct::baz;
  std::forward<V>(v)("baz", (std::forward<Ss>(ss).*baz_ptr)...);
}

That at least will cause a compile error if they use it with mismatching types. But, it's also happening too late -- it's happening after the template types are resolved, and after overload resolution I guess.

I thought about using SFINAE, like, instead of returning void, using std::enable_if_t and checking some expression std::is_same<std::remove_cv_t<std::remove_reference_t<...>> for each type in the parameter pack.

But for one, that SFINAE expression is pretty complicated, and for two, it also has a drawback -- suppose someone has a derived class struct my_other_struct : my_struct { ... }, and they want to use it with the visitor mechanism, so some of the parameters are my_struct and some are my_other_struct. Ideally the system would convert all the references to my_struct and apply the visitor that way, and afaik the example I gave above with the member pointers foo_ptr, bar_ptr, baz_ptr would do the right thing there, but it's not even clear to me how to write a constraint like that with SFINAE -- I would have to try to find a common base of all the parameters I guess?

Is there a good way to reconcile those concerns in general?

Chris Beck
  • 15,614
  • 4
  • 51
  • 87
  • 4
    `std::common_type`? – cpplearner Jul 22 '16 at 14:25
  • Do you want them to all be of a specific known type, or just to be of the same, unknown type? – Quentin Jul 22 '16 at 14:28
  • @cpplearner: Oh, that is quite nice, I did not know about that – Chris Beck Jul 22 '16 at 14:33
  • @Quentin: I guess if it would make a difference I'd like to see an example. I'd like to know a good way to do the more general one -- of the same, unknown type – Chris Beck Jul 22 '16 at 14:34
  • It does not make sense to `forward(v)` more than once. – aschepler Aug 13 '16 at 22:23
  • @aschepler: Interesting, but I think it's ok. We are forwarding into a method call, we aren't actually moving the visitor. The only thing this does is enable `&&` ref-qualified `operator()`. So I think the only way it could cause a problem would be if `operator()` decides to call `delete this` as a result of the `&&` qualifiers, or similar. I certainly do want to forward `V` in regards to `const`. So I think forwarding `V` repeatedly here is okay. You are welcome to try to convince me otherwise. – Chris Beck Aug 14 '16 at 15:44

7 Answers7

15

With std::common_type, this is straightforward:

template <class... Args, class = std::common_type_t<Args...>>
void foo(Args &&... args) {

}

This will only be guaranteed to be SFINAE-friendly from C++17 onwards, though. Clang and GCC both implement it that way already.

slawekwin
  • 6,270
  • 1
  • 44
  • 57
Quentin
  • 62,093
  • 7
  • 131
  • 191
  • @JohanLundberg I had put it there to emulate parameter-passing, but after some testing it actually breaks pass-by-reference. Fixed. – Quentin Jul 23 '16 at 14:09
  • 1
    `common_type` determines if it exists a common type to which all the others can be implicitly converted. This does not mean that they are all of the same type. – skypjack Jul 28 '16 at 21:57
  • @skypjack the last paragraph in OP indicates that this is the desired behaviour. – Quentin Jul 29 '16 at 07:36
7

Here's a type trait that you can use in a static_assert or std::enable_if at your leisure.

template <class T, class ... Ts>
struct are_all_same : conjunction<std::is_same<T, Ts>...>{};

template <class Ts...>
struct conjunction : std::true_type{};

template <class T, class ... Ts>
struct conjunction<T, Ts...> :
    std::conditional<T::value, conjunction<Ts...>, std::false_type>::type {};

It quite simply checks each type with the first one and fails if any are different.

I think using std::common_type would look something like this:

    template <class ... Args>
    typename std::common_type<Args...>::type common_type_check(Args...);

    void common_type_check(...);

    template <class ... Ts>
    struct has_common_type :
        std::integral_constant<
            bool,
            !std::is_same<decltype(common_type_check(std::declval<Ts>()...)), void>::value> {};

Then you can do static_assert(std::has_common_type<Derived, Base>::value, "")

Of course, this method isn't foolproof as common_type has some restrictions when it comes to base classes:

struct A    {};
struct B : A{};
struct C : A{};
struct D : C{};
struct E : B{};

static_assert(has_common_type<E, D, C, A, B>::value, ""); //Fails
static_assert(has_common_type<A, B, C, D, E>::value, ""); //Passes

This is because the template first tries to get the common type between D and E (ie, auto a = bool() ? D{}: E{}; fails to compile).

SirGuy
  • 10,660
  • 2
  • 36
  • 66
7

What you really want is something like:

template<typename T, T ... args>
void myFunc(T ... args);

But clearly the above is not legal syntax. You can get around this problem however, with a templated using to help you. So the idea is this:

template<typename T, size_t val>
using IdxType = T;

The above has no real purpose: an IdxType<T, n> is just a T for any n. However, it lets you do this:

template<typename T, size_t ... Indices>
void myFunc(IdxType<T, Indices> ... args);

Which is great, as this is precisely what you need to get a variadic set of identically-typed parameters. The only problem that remains is that you can't do things like myFunc(obj1, obj2, obj3), as the compiler won't be able to deduce the required Indices - you will have to do myFunc<1,2,3>(obj1, obj2, obj3), which is ugly. Fortunately, you can get away from this by wrapping in a helper function that takes care of the index generation for you using make_index_sequence.

Below is a full example, which is something similar to your visitor (live demo here):

template<typename T, size_t sz>
using IdxType = T;

struct MyType
{};

struct Visitor
{
    void operator() (const MyType&) 
    {
        std::cout << "Visited" << std::endl;
    }
};

template <typename V>
void apply_visitor(std::index_sequence<>, V && v) 
{
}

template <typename V, typename T, size_t FirstIndex, size_t ... Indices>
void apply_visitor(std::index_sequence<FirstIndex, Indices...>, V && v, T && first, IdxType<T, Indices> && ... ss) {
    std::forward<V>(v)(std::forward<T>(first));
    apply_visitor(std::index_sequence<Indices...>(), std::forward<V>(v), std::forward<T>(ss) ...);
}

template <typename V, typename T, typename ... Rest>
void do_apply_visitor(V && v, T && t, Rest && ... rest )
{
    apply_visitor(std::make_index_sequence<sizeof...(Rest)+1>(), v, t, rest ... );
}

int main()
{
    Visitor v;

    do_apply_visitor(v, MyType{}, MyType{}, MyType{});

    return 0;
}
Smeeheey
  • 9,906
  • 23
  • 39
3

suppose that it really only makes sense to call my function in the case where each argument has the same type -- any number of arguments would be okay though.

In this case, why not use std::initializer_list?

template <typename T>
void func(std::initializer_list<T> li) {
    for (auto ele : li) {
        // process ele
        cout << ele << endl;
    }
}

As @Yakk mentioned in the comment, you might want to avoid const copies. In that case, you can copy pointers to std::initializer_list:

// Only accept pointer type
template <typename T>
void func(std::initializer_list<T> li) {
    for (auto ele : li) {
        // process pointers, so dereference first
        cout << *ele << endl;
    }
}

Or specialize func for pointers:

// Specialize for pointer
template <typename T>
void func(std::initializer_list<T*> li) {
    for (auto ele : li) {
        // process pointers, so dereference first
        cout << *ele << endl;
    }
}

my_struct a, b, c;
func({a, b, c}); // copies
func({&a, &b, &c}); // no copies, and you can change a, b, c in func
for_stack
  • 21,012
  • 4
  • 35
  • 48
3

This takes an arbitrary In type and moves its r/lvalue reference-ness over to an Out type in an implicit cast.

template<class Out>
struct forward_as {
  template<class In,
    std::enable_if_t<std::is_convertible<In&&,Out>{}&&!std::is_base_of<Out,In>{},int>* =nullptr
  >
  Out operator()(In&& in)const{ return std::forward<In>(in); }
  Out&& operator()(Out&& in)const{ return std::forward<Out>(in); }
  template<class In,
    std::enable_if_t<std::is_convertible<In&,Out&>{},int>* =nullptr
  >
  Out& operator()(In& in)const{ return in; }
  template<class In,
    std::enable_if_t<std::is_convertible<In const&,Out const&>{},int>* =nullptr
  >
  Out const& operator()(In const& in)const{ return in; }
};

With this, here is our n-ary apply_visitor:

template <typename V, typename ... Ss,
  decltype(std::void_t<
    std::result_of_t<forward_as<my_struct>(Ss)>...
  >(),int())* =nullptr
>
void apply_visitor(V && v, Ss && ... ss) {
  auto convert = forward_as<my_struct>{};

  std::forward<V>(v)("foo", (convert(std::forward<Ss>(ss)).foo)...);
  std::forward<V>(v)("bar", (convert(std::forward<Ss>(ss)).bar)...);
  std::forward<V>(v)("baz", (convert(std::forward<Ss>(ss)).baz)...);
}

which fails to match if forward_as<my_struct> fails to evaluate on Ss.

live example

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
0

A possible solution would be using a compile time function like are_same in the following example:

#include <type_traits>

template<typename T, typename... O>
constexpr bool are_same() {
    bool b = true;
    int arr[] = { (b = b && std::is_same<T, O>::value, 0)... };
    return b;
}

int main() {
    static_assert(are_same<int, int, int>(), "!");
    static_assert(not are_same<int, double, int>(), "!");
}

Use it as it follows:

template <typename... Ts>
void func(Ts &&... ts) {
    static_assert(are_same<Ts...>(), "!");
    step_one(std::forward<Ts>(ts)...);
    step_two(std::forward<Ts>(ts)...);
}

You'll have a nice compile time error message as requested.

skypjack
  • 49,335
  • 19
  • 95
  • 187
-4

I think you can make a function like this and check the arguments inside of your function.

template <typename T, typename... Args> bool check_args(T a, Args args)
{
static string type;
if(type == "") type = typeid(a).name;
else if(type != typeid(a).name) return false;
else return check_args(args...);
}
bool check_args() {return true;}
Zeta
  • 913
  • 10
  • 24
  • 1
    This won't allow to check in a `static_assert` or to disable an overload with `enable_if` – SirGuy Jul 22 '16 at 15:58