Introduction
A number of free functions in the C++ standard library are intended to be both generic and customizable. Since we want to take advantage of two distinct features of overload resolution depending on what the actual type of T
turns out to be, you need to do the infamous “std two-step”. This is most well-known with the swap
function.
You may have been taught the idiom and follow it without knowing why it is needed.
template <typename T> void foo (T& a)
{
T b;
⋮
using std::swap; swap (a,b); }
This is necessary not just for swap
but for a whole list of free functions, including begin
, end
, and size
.
Meanwhile, we are not supposed to leak declarations into global (or namespace) scope, especially in headers. So, the using
needs to go inside a block of some kind. But, there are cases where we want to use one of these library functions outside of any suitable scoping block. Some of these cases can be worked around by adding a details
namespace or the like. Other cases are not so easily fixed!
template <typename R>
auto myfunction1 (const R& input) -> decltype(std::begin(input)) { ⋯ }
template <typename R>
C<R>::C (const R& val) : it{ std::begin(val) } { ⋯ }
The signature of a function can have a computed return type and noexcept
specification, plus default argument values. These are all outside of the body, so where can you put the using std::begin
?
Constructors have the base-member initializer list. Again, this is outside of the function’s block.
Why the Two-step is Necessary
Consider if a type T
is a class that has its own swap
(or begin
, size
, etc.) defined for it. Those functions will be in the same namespace as the class, so you must call them without any qualifier in order to use argument dependant lookup (ADL).
Or, suppose T
is a built-in primitive type, or even a class that is perfectly happy with the default implementation. You would need to find the version in the std
namespace, but ADL is not going to look there.
The two-step idiom allows for both cases. The using
declaration brings the std
version into scope, and that will be combined with the results of ADL when selecting a function.
Prefer the Free Functions in General Use
Current wisdom is to prefer free functions. You don’t want to do things one way or a different way depending on the context. Also, it is generally good to program as if you are writing a template, to some extent, even when you are not. It is common during maintenance and enhancement for the types of things to change. If your code was resilient to such things, you will have a lot less ripple to deal with.
How to Eliminate the Two-step
It is possible to write a wrapper function that calls the desired function. The wrapper has the two-step coded in its body. There are a number of nuances: make sure the arguments use “perfect forwarding”, make sure you match the noexcept status of the wrapped function, and make sure you provide SFINAE that matches the availability of any matching function!
I will have wrappers with the same names but for a capital letter: Swap
calls swap
, etc.
Now, how to make sure your wrapper is called, rather than some other function of the same name? Eric Niebler makes an argument that these customization points should have been function-call objects, not functions.
I get the same benefits by doing this with my wrappers. Specifically, if Swap
is a variable name, not a function name, then none of the overload resolution stuff applies, at all! The compiler looks outward in lexical scopes and finds the name Swap
. It is not a function name, so the compiler is done looking. In particular, it will not look for other versions of the function in all the namespaces associated with the argument types.
How to Package the Wrappers
We want a namespace that has only the wrappers in it. The user can do using namespace twostep;
and get these functions in the current scope. Specifically, it must not also pull in the std::
versions of the functions! But, we have the problem explained at the beginning so can’t just put the using std::swap
inside the wrapper function — the return type metacalculation and the noexcept
determination both need to use the underlying function as well. The using std::
(step 1) must be in a scope outside of the wrapper functions.
The solution is to make a sandwich of namespaces.
namespace detail_twostep_wrapper {
using std::swap;
namespace twostep_inner {
inline auto Swap = ⋯
}
}
namespace twostep = detail_twostep_wrapper::twostep_inner;
At this point, the code sees namespace twostep
which contains only the wrapper functions. The stuff inside namespace detail_twostep_wrapper
will not bother anyone, since no types are defined in it.
How to Write the Perfect Wrapper
We know that the body of the wrapper function calls the function being wrapped, and the std::
version is already been brought into scope, as set up in the previous listing. We just need to use perfect forwarding:
Let’s use begin
as our strawman, since it only has one argument. Also, I’ll start with ordinary functions (not function-call objects) for simplicity.
template <typename T> auto Begin (auto&& r)
{
return begin(std::forward(decltype(r)>(r));
}
The call to begin(r)
shows the perfect forwarding idiom.
Unfortunately, this first attempt is not good enough. If there is no begin
to forward to, you get a compiler error telling you arcane details of the template instantiation. Instead, we want Begin
to disappear when begin
disappears. That means adding some SFINAE stuff.
The easiest way to do that is with the return value. Repeating the body as part of the signature makes it subject to SFINAE.
template <typename T> auto Begin (auto&& r) -> decltype(begin(std::forward<decltype(r)>(r)))
{
return begin(std::forward(decltype(r)>(r));
}
Now, we still have the issue that our wrapper is not marked noexcept
. Again, we want to give the same status as the wrapped function.
template <typename T> auto Begin (auto&& r)
noexcept(noexcept(xname(std::forward<decltype(r)>(r))))
-> decltype(begin(std::forward<decltype(r)>(r)))
{
return begin(std::forward(decltype(r)>(r));
}
The perfect wrapper is called the “you have to type it three times” idiom.
How to Make It an Object
If you read Eric’s blog post, you’ll see that his listing is rather long and quite cryptic.
Finally, we define a std::begin
object of type std::__detail::__begin_fn
in a round-about sort of way, the details of which are not too relevant.
He had to battle with two major hurdles:
- putting an initialized global variable in a header
- polymorphic lambdas were not available in C++11, and he could not get the SFINAE to work with lambdas in C++14
I’m writing with (nearly!) C++17. A new feature is specifically designed to address problem 1. Now, you can write inline variables. This means you can write the initialization value in the header and not need any secondary definition placed in exactly one cpp file. So, goodbye to “round-about sort of way” obfuscation.
With an object having a polymorphic function call operator, you will never have the variable itself disappear due to SFINAE or concept checks. I found that if I use SFINAE on the lambda, I get a rather short detailed error message that’s not hard to figure out. Four lines of detail, with the first being “no matching overloaded function found” and the 4th being the argument type.
Without the SFINAE on the lambda, I get a much longer error scroll going into details about the template and its caller.
The long details might be better, since it lets you find the location of the caller, as well as the problem location of the generic lambda. I think this is a general problem with using function-call objects: the error is not in the finding something with the right name. This is simply a need to improve the useful error messages of the compiler — hopefully that will happen if this idiom gains popularity and is used in major libraries. I may add a simple macro to disable the SFINAE checks if that is helpful in tracking down errors.
Here is the final version of the wrapper function as an object:
inline constexpr auto Begin = [](auto&& r)
noexcept(noexcept(begin(std::forward<decltype(r)>(r))))
-> decltype( begin(std::forward<decltype(r)>(r)))
{ return begin(std::forward<decltype(r)>(r)); };
The Code
The header file twostep.h contains all the functions that need the two-step. It is stand-alone, not needing anything other than the standard headers containing the functions being wrapped. You can easily copy it into your project without dealing with the rest of the library.
The current version can always be found on Github.
Update
In C++20, these "neibloids" as they have come to be called are now included in the standard library. Simply use the same names from std::ranges
instead of std
.