Variadic Using and How to Use It


C++17 allows parameter pack expansion inside using statements. This feature is sometimes called variadic-using and the paper is here. Unfortunately, I found searching for “variadic using” with Google to not be all that helpful. In this post I will provide a concrete example of what this feature can be used for, since it might seem like a bit of a strange idea at first.

The idea for this post came about from wanting to use call operator overloading in a lambda expression, which is not something provided by the language. Basically, I would like to write a lambda expression with two call operators defined that take different arguments. I can do this for a struct, but that cannot be done inline like lambda expressions. This problem is easily solved with a variadic-using. Here is the code:

1
2
3
4
5
6
7
8
9
10
11
template <class... Lambdas>
struct overload : Lambdas... {
  explicit overload(Lambdas... lambdas) : Lambdas(std::move(lambdas))... {}

  using Lambdas::operator()...;
};

template <class... Lambdas>
overload<Lambdas...> make_overload(Lambdas... lambdas) {
  return overload<Lambdas...>(std::move(lambdas)...);
}

The make_overload is a helper function to do the type deduction for us, similar in spirit to std::make_tuple.1 What line 5 (the variadic-using) does in pull in the call operator of all the Lambdas into the scope of overload so that they are available for overload resolution.

Now what this allows you to do is use overloading as a pseudo-SFINAE by having the lambdas take as an argument a std::integral_constant<bool, B> and having a type trait fill the value of B at the call site. Here is some code that uses this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <iostream>
#include <type_traits>

template <class... Lambdas>
struct overload : Lambdas... {
  explicit overload(Lambdas... lambdas) : Lambdas(std::move(lambdas))... {}

  using Lambdas::operator()...;
};

template <class... Lambdas>
overload<Lambdas...> make_overload(Lambdas... lambdas) {
  return overload<Lambdas...>(std::move(lambdas)...);
}

struct my_type1 {
  int func(int a) { return 2 * a; }
};

struct my_type2 {};

template <class T, class = std::void_t<>>
struct has_func : std::false_type {};

template <class T>
struct has_func<
    T, std::void_t<decltype(std::declval<T>().func(std::declval<int>()))>>
    : std::true_type {};

static_assert(has_func<my_type1>::value, "");
static_assert(not has_func<my_type2>::value, "");

template <class T>
void func(T t) {
  auto my_lambdas = make_overload(
      [](auto& f, std::integral_constant<bool, true>) {
        std::cout << "True " << f.func(2) << "\n";
      },
      [](auto& f, std::integral_constant<bool, false>) {
        std::cout << "False\n";
      });
  my_lambdas(t, std::integral_constant<bool, has_func<T>::value>{});
}

int main() {
  auto my_lambdas = make_overload(
      [](std::integral_constant<bool, true>) { std::cout << "True\n"; },
      [](std::integral_constant<bool, false>) { std::cout << "False\n"; });
  my_lambdas(std::integral_constant<bool, true>{});
  my_lambdas(std::integral_constant<bool, false>{});

  func(my_type1{});
  func(my_type2{});
}

This prints out:

1
2
3
4
True
False
True 4
False

What you notice is that the type trait has_func is used on line 42, which then calls the correct generic lambda in my_lambdas. Of course, you are not limited to selecting between two lambdas, you can select between arbitrary many and then use a single std::integral_constant<int, I> or multiple std::integral_constant<bool, B> arguments. At the call site things are little trickier since you need to do something like std::integral_constant<int, trait1<T>::value + 2 * trait2::value> for the former, and std::integral_constant<bool, trait1<T>::value>{}, std::integral_constant<bool, trait2<T>::value>{} for the latter.

Summary

In this post we explored variadic-using declarations to allow SFINAE-like behavior in generic lambdas. A similar implementation is possible in C++14 but that’s for another post. Overloading for SFINAE-like behavior greatly extends the usefulness and usability of lambda expressions, since overloading of functions and SFINAE are common and powerful techniques for developing generic code. The code I showed above is available on my GitHub with the name variadic-using.cpp.

  1. make_overload can be replaced with a deduction guideline, but that is not yet supported in a release build of Clang, so I am avoiding it for now. 

Back to blog

comments powered by Disqus