Passing overload sets to functions
Passing functions to functions is becoming increasingly prevalent in C++. With common advice being to prefer algorithms to loops, new library features like
std::visit, lambdas being incrementally beefed up12 and C++ function programming talks consistently being given at conferences, it’s something that almost all C++ programmers will need to do at some point. Unfortunately, passing overload sets or function templates to functions is not very well supported by the language. In this post I’ll discuss a few solutions and show how C++ still has a way to go in supporting this well.
We have some generic operation called
foo. We want a way of specifying this function which fulfils two key usability requirements.
1- It should be callable directly without requiring manually specifying template arguments:
2- Passing it to a higher-order function should not require manually specifying template arguments:
A simple first choice would be to make it a function template:
This fulfils the first requirement, but not the second:
That’s no good.
A second option is to write
foo as a function object with a call operator template:
We are now required to create an instance of this type whenever we want to use the function, which is okay for passing to other functions, but not great if we want to call it directly:
We have similar problems when we have multiple overloads, even when we’re not using templates:
We’re going to need a different solution.
As an intermediate step, we could use the normal function template approach, but wrap it in a lambda whenever we want to pass it to another function:
That’s not great. It’ll work in some contexts where we don’t know what template arguments to supply, but it’s not yet suitable for all cases. One improvement would be to add perfect forwarding:
But wait, we want to be SFINAE friendly, so we’ll add a trailing return type:
Okay, it’s getting pretty crazy and expert-only at this point. And we’re not even done! Some contexts will care about
So the solution is to write this every time we want to pass an overloaded function to another function. That’s probably a good way to make your code reviewer cry.
The above is functionally equivalent to the triplicated monstrosity in the example before. Even better, if P0834: Lifting overload sets into objects was accepted, we could write:
That lifts the overload set into a single function object which we can pass around. Unfortunately, all of those proposals have been rejected. Maybe they can be renewed at some point, but for now we need to make do with other solutions. One such solution is to approximating
foo with a macro (I know, I know).
Now our higher-order function call becomes:
Okay, so there’s a macro in there, but it’s not too bad (you know we’re in trouble when I start trying to justify the use of macros for this kind of thing). So
LIFT is at least some solution.
Making function objects work for us
You might recall from a number of examples ago that the problem with using function object types was the need to construct an instance whenever we needed to call the function. What if we make a global instance of the function object?
This works if you’re able to have a single translation unit with the definition of the global object. If you’re writing a header-only library then you don’t have that luxury, so you need to do something different.
This might look innocent, but it can lead to One-Definition Rule (ODR) violations3:
foo is declared
static, each Translation Unit (TU) will get its own definition of the variable. However,
also_sad will instantiate
oh_no which will get different definitions of
&foo. This is undefined behaviour by
In C++17 the solution is simple:
inline allows the variable to be multiply-defined, and the linker will throw away all but one of the definitions.
An advantage of the function object approach is that function objects designed carefully make for much better customisation points than the traditional techniques used in the standard library. See Eric Niebler’s blog post and standards paper for more information.
A disadvantage is that now we need to write all of the functions we want to use this way as function objects, which is not great at the best of times, and even worse if we want to use external libraries. One possible solution would be to combine the two techniques we’ve already seen:
Now we can use
lift::foo instead of
lib::foo and it’ll fit the requirements I laid out at the start of the post. Unfortunately, I think it’s possible to hit ODR-violations with this due to possible difference in closure types cross-TU. I’m not sure what the best workaround for this is, so input is appreciated.
I’ve given you a few solutions to the problem I showed at the start, so what’s my conclusion? C++ still has a way to go to support this paradigm of programming, and teaching these ideas is a nightmare. If a beginner or even intermediate programmer asks how to pass overloaded functions around – something which sounds like it should be fairly easy – it’s a real shame that the best answers I can come up with are “Copy this macro which you have no chance of understanding”, or “Make function objects, but make sure you do it this way for reasons which I can’t explain unless you understand the subtleties of ODR4”. I feel like the language could be doing more to support these use cases.
Maybe for some people “Do it this way and don’t ask why” is an okay answer, but that’s not very satisfactory to me. Maybe I lack imagination and there’s a better way to do this with what’s already available in the language. Send me your suggestions or heckles on Twitter @TartanLlama.
Thanks to Michael Maier for the motivation to write this post; Jayesh Badwaik, Ben Craig, Michał Dominiak and Kévin Boissonneault for discussion on ODR violations; and Eric Niebler, Barry Revzin, Louis Dionne, and Michał Dominiak (again) for their work on the libraries and standards papers I referenced.
Let me know what you think of this article on twitter @TartanLlama or leave a comment below!