Table of Contents

Learn how the overload pattern works for std::variant visitation and how it changed with C++20 and C++23.

While I was doing research for my book and blog posts about C++17 several times, I stumbled upon this pattern for visitation of std::variant:

template<class... Ts> struct overload : Ts... { using Ts::operator()...; };
template<class... Ts> overload(Ts...) -> overload<Ts...>;

With the above pattern, you can provide separate lambdas “in-place” for visitation.

It’s just two lines of compact C++ code but packs some exciting techniques.

Let’s see how this works and go through the three new C++17 features that make this pattern possible.

Changelog

  • Updated on 18th Septeber 2023: C++23 updates, and Compiler Explorer examples.
  • Updated on 13th January 2020: better description for the whole article and C++ 20 features were mentioned - CTAD for aggregates.
  • Initial version on 11th Feb 2019

Intro  

The code mentioned at the top of the article forms a pattern called overload (or sometimes overloaded), and it’s primarily valid for std::variant visitation.

With such helper code, you can write:

template<class... Ts> struct overload : Ts... { using Ts::operator()...; };
template<class... Ts> overload(Ts...) -> overload<Ts...>;

std::variant<int, float, std::string> intFloatString { "Hello" };
std::visit(overload  {
      [](const int& i) { std::cout << "int: " << i; },
      [](const float& f) { std::cout << "float: " << f; },
      [](const std::string& s) { std::cout << "string: " << s; }
    },
    intFloatString
);

The output:

string: Hello

Little reminder: std::variant is a helper vocabulary type, a discriminated union. As a so-called sum-type it can hold non-related types at runtime and switch between them by reassignmentstd::visit allows you to invoke an operation on the currently active type from the given variant. Read more in my blog post Everything You Need to Know About std::variant from C++17.

Without the overload, you’d have to write a separate class or struct with three overloads for the call operator `()':

struct PrintVisitor
{
    void operator()(int& i) const {
        std::cout << "int: " << i; }

    void operator()(float& f) const {
        std::cout << "float: " << f;
    }

    void operator()(std::string& s) const {
        std::cout << "string: " << s;
    }
};

std::variant<int, float, std::string> intFloatString { "Hello" };
std::visit(PrintVisitor(), intFloatString);

So, how does the overload pattern work? Why do we need to inherit from lambdas there?

As you might already know, the compiler conceptually expands lambda expression into a uniquely-named type with operator().

In the overload pattern, we inherit from several lambdas and then expose their operator() for std::visit. That way, you write overloads “in place”.

What are the C++17 features that compose the pattern?

Here’s the list:

  • Pack expansions in using declarations - short and compact syntax with variadic templates.
  • Custom template argument deduction rules - that allow converting a list of lambda objects into a list of base classes for the overloaded class. (note: not needed in C++20!)
  • Extension to aggregate Initialization - before C++17, you couldn’t aggregate initialize type derived from other types.

New C++17 Features  

Let’s explore section by section the new elements that compose the overload pattern. That way, we can learn a few exciting things about the language.

Using Declarations  

As you can see, we have three features to describe, and it’s hard to tell which one is the simplest to explain.

But let’s start with using. Why do we need it at all?

To understand that, let’s write a simple type that derives from two base classes:

#include <iostream>
    
struct BaseInt {
    void Func(int) { std::cout << "BaseInt...\n"; }
};

struct BaseDouble {
    void Func(double) { std::cout << "BaseDouble...\n"; }
};

struct Derived : public BaseInt, BaseDouble {
    //using BaseInt::Func;
    //using BaseDouble::Func;
};

int main() {
    Derived d;
    d.Func(10.0);
}

We have two base classes that implement Func. We want to call that method from the derived object.

Will the code compile?

When doing the overload resolution set, C++ states that the Best Viable Function must be in the same scope.

So GCC reports the following error:

error: request for member 'Func' is ambiguous

See a demo here @Coliru

That’s why we have to bring the functions into the scope of the derived class.

We have solved one part, and it’s not a feature of C++17. But how about the variadic syntax?

The issue was that before C++17, using... was not supported.

In the paper Pack expansions in using-declarations P0195R2 - there’s a motivating example that shows how much extra code was needed to mitigate that limitation:

template <typename T, typename... Ts>
struct Overloader : T, Overloader<Ts...> {
     using T::operator();
     using Overloader<Ts...>::operator();
     // […]
};
    
template <typename T> struct Overloader<T> : T {
    using T::operator();
};

In the example above, in C++14, we had to create a recursive template definition to be able to use using. But now we can write:

template <typename... Ts>
struct Overloader : Ts... {
    using Ts::operator()...;
    // […]
};

It’s much simpler now!

Okay, but how about the rest of the code?

Custom Template Argument Deduction Rules  

We derive from lambdas and then expose their operator() as we saw in the previous section. But how can we create objects of this overload type?

As you know, there’s no way to know up-front the type of the lambda, as the compiler has to generate some unique type name for each of them. For example, we cannot just write:

overload<LambdaType1, LambdaType2> myOverload { ... } // ???
// what is LambdaType1 and LambdaType2 ??

The only way that could work would be some make function (as template argument deduction works for function templates since, like always):

template <typename... T>
constexpr auto make_overloader(T&&... t) {
    return Overloader<T...>{std::forward<T>(t)...};
}

With template argument deduction rules added in C++17, we can simplify the creation of common template types, and the make_overloader function is unnecessary.

For example, for simple types, we can write:

std::pair strDouble { std::string{"Hello"}, 10.0 };
// strDouble is std::pair<std::string, double>

There’s also an option to define custom deduction guides. The Standard library uses a lot of them, for example, for std::array:

template <class T, class... U>
array(T, U...) -> array<T, 1 + sizeof...(U)>;

And the above rule allows us to write:

array test{1, 2, 3, 4, 5};
// test is std::array<int, 5>

For the overload pattern, we can write:

template<class... Ts> overload(Ts...) -> overload<Ts...>;

Now, we can type

overload myOverload { [](int) { }, [](double) { } };

And the template arguments for overload will be correctly deduced. In our case, the compiler will know the types of lambdas.

Let’s now go to the last missing part of the puzzle - aggregate Initialization.

Extension to Aggregate Initialisation  

This functionality is relatively straightforward: we can now initialize a type derived from other types.

As a reminder: from dcl.init.aggr:

An aggregate is an array or a class with:

  • no user-provided, explicit, or inherited constructors
  • no private or protected non-static data members
  • no virtual functions, and
  • no virtual, private, or protected base classes

For example (sample from the spec draft):

struct base1 { int b1, b2 = 42; };

struct base2 {
  base2() { b3 = 42; }
  int b3;
};

struct derived : base1, base2 {
   int d;
};
    
derived d1{{1, 2}, {}, 4};
derived d2{{}, {}, 4};

Initializes d1.b1 with 1, d1.b2 with 2, d1.b3 with 42, d1.d with 4, and d2.b1 with 0, d2.b2 with 42, d2.b3 with 42, d2.d with 4.

In our case, it has a more significant impact. Because for the overload class, without the aggregate initialization, we’d had to implement the following constructor:

struct overload : Fs... 
{
  template <class ...Ts>
  overload(Ts&& ...ts) : Fs{std::forward<Ts>(ts)}...
  {} 
    
  // ...
}

It’s a lot of code to write, and it probably doesn’t cover all of the cases like noexcept.

With aggregate initialization, we “directly” call the lambda constructor from the base class list, so there’s no need to write it and forward arguments to it explicitly.

Demo  

You can play with examples at Compiler Explorer:

C++20 Updates  

With each C++ revision, there’s usually a chance to write even more compact code. With C++20, it’s possible to have even shorter syntax.

Why?

It’s because in C++20, there are extensions to Class Template Argument Deduction, and aggregates are automatically handled. That means that there’s no need to write a custom deduction guide.

For a simple type:

template <typename T, typename U, typename V>
struct Triple { T t; U u; V v; };

In C++20, you can write:

Triple ttt{ 10.0f, 90, std::string{"hello"}};

And T will be deduced as float, U as int, and V as std::string.

The overloaded pattern in C++20 is now just:

template<class... Ts> struct overload : Ts... { using Ts::operator()...; };

The proposal for this feature is available in P1021 and also P1816 (wording).

Run this code @Compiler Explorer and check how it works in C++20.

C++23 additions and safer overload  

In C++23, the pattern wasn’t reduced much, but we can update it with a cool addition to make it safer.

I got the inspiration from Andreas Fertig’s blog post: Visiting a std::variant safely.

template<class... Ts>
struct overload : Ts...
{
  using Ts::operator()...;

  consteval void operator()(auto) const {
    static_assert(false, "Unsupported type");
  }
};

The code is now much bigger, not just one line of code. But now, you can avoid situations when you forget to implement an overload for a specific type in a variant. In that case, the updated pattern will report you an error at compile time.

Run the example @Compiler Explorer

The C++23 update comes from the update for static_assertregarding dependency on the template parameter. Before C++23, static_assert(false) would always generate an error, while in C++23, only when the template is instantiated. See more in my article for Patrons: static_assert improvements for C++23, always_false idiom | Patreon

Summary  

The overload pattern is a fascinating thing. It demonstrates several C++ techniques, gathers them together, and allows us to write shorter syntax.

In C++14, you could derive from lambdas and build similar helper types, but only with C++17 you can significantly reduce boilerplate code and limit potential errors. With C++20, we’ll get even shorter syntax as CTAD will work with aggregates.

You can read more in the proposal for overload P0051 (it was not accepted for C++20, but it’s worth to see discussions and concepts behind it). The pattern presented in this blog post supports only lambdas, and there’s no option to handle regular function pointers. In the paper, you can see a much more advanced implementation that tries to handle all cases.

And if you want to learn more about techniques using lambda expressions see my book: C++ Lambda Story @Leanpub or @Amazon in paperback.

Your Turn

  • Have you used std::variant and visitation mechanism?
  • Have you used overload pattern?

Resources