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.
An example
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.
Lambdas and LIFT
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 noexcept
:
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.
What would be nice is if P0573: Abbreviated Lambdas for Fun and Profit and P0644: Forward without forward
were accepted into the language. That’d let us write this:
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:
Since foo
is declared static
, each Translation Unit (TU) will get its own definition of the variable. However, sad
and also_sad
will instantiate oh_no
which will get different definitions of foo
for &foo
. This is undefined behaviour by [basic.def.odr]/12.2
.
In C++17 the solution is simple:
The inline
allows the variable to be multiply-defined, and the linker will throw away all but one of the definitions.
If you can’t use C++17, there are a few solutions given in N4424: Inline Variables. The Ranges V3 library uses a reference to a static member of a template class:
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.
Conclusion
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.
-
P0624: Default constructible and assignable stateless lambdas ↩
-
Disclaimer: I don’t understand all the subtleties of ODR. ↩
Let me know what you think of this article on twitter @TartanLlama or leave a comment below!