Click here to Skip to main content
15,867,308 members
Articles / General Programming / Algorithms

Avoid the “std 2-step”

Rate me:
Please Sign up or sign in to vote.
4.85/5 (8 votes)
27 May 2021CPOL7 min read 11.8K   4   3
Using swap etc. from generic code must be done properly. Here’s how to fix it for good.

two-step cartoon

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.

C++
template <typename T> void foo (T& a)
{
    T b;
     ⋮
    using std::swap;  // step 1
    swap (a,b);       // step 2
}

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!

C++
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.

C++
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.

C++
template <typename T>   // first attempt
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.

C++
template <typename T>   // second attempt
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.

C++
template <typename T>   // third attempt
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:

  1. putting an initialized global variable in a header
  2. 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:

C++
inline constexpr auto Begin = [](auto&& r)
    noexcept(noexcept(begin(std::forward<decltype(r)>(r)))) /* ask if the code is noexcept */
    -> decltype(      begin(std::forward<decltype(r)>(r)))  /* using return type to do SFINAE */
        {      return begin(std::forward<decltype(r)>(r));  /* the real body to execute */ };

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.

This article was originally posted at https://github.com/jdlugosz/d3

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Ștefan-Mihai MOGA12-Jun-21 12:20
professionalȘtefan-Mihai MOGA12-Jun-21 12:20 
GeneralMy vote of 3 Pin
Vincent Radio1-Jun-21 20:00
professionalVincent Radio1-Jun-21 20:00 
QuestionMessage Closed Pin
31-May-21 23:03
Apoorva Gupta 202131-May-21 23:03 
PraiseNice Description! Pin
koothkeeper29-May-18 9:10
professionalkoothkeeper29-May-18 9:10 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.