Implementation divergence on swapping bools within vectors

Today, while inventing questions for my next C++ Pub Quiz, I ran into a fairly obscure and meaningless divergence among vendors’ implementations of vector<bool>::reference. Given:

std::vector<bool> v = {true, true};

The correct way to swap v[0] with v[1] is of course swap(v[0], v[1]), optionally preceded by using std::swap; see “What is the std::swap two-step?” (2020-07-11). But look at all these wrong things you can try!

  libstdc++ libc++ MSVC
swap(v[0], v[1]); OK OK OK
swap<>(v[0], v[1]); error OK error
std::swap(v[0], v[1]); OK OK OK pre-C++20
std::swap<>(v[0], v[1]); error OK OK pre-C++20
swap(v[0], {}); OK error error
swap<>(v[0], {}); error error error
std::swap(v[0], {}); OK error error
std::swap<>(v[0], {}); error error error

I found it interesting that std::swap(v[0], v[1]) doesn’t compile on Microsoft’s STL. This is totally fine, according to my reading of the paper standard; I even think it’s a good thing; but it still surprises me that they can get away with it. Also, it’s an error only in MSVC’s -std:c++latest mode; it compiles fine in -std:c++17 mode. I haven’t tried to track down why.

UPDATE, 2021-06-29: Casey Carter explains that Microsoft’s std::_Vb_reference<T> has a hidden-friend swap in both C++17 and C++20. The trick is that MSVC’s -permissive mode permits many non-conforming extensions, and one of those extensions is that hidden friends aren’t actually hidden against qualified lookup. So in MSVC’s -permissive mode, a qualified call to std::swap(v[0], v[1]) successfully finds the hidden friend. Finally, -permissive is MSVC’s default prior to C++20; but when you turn on -std:c++latest, it implicitly turns off -permissive. Passing -std:c++17 -permissive- rejects the qualified call (Godbolt), and -std:c++latest -permissive accepts it.


I also found it amusing that std::swap(v[0], {}), on GNU libstdc++, right now has the same codegen as std::exchange(v[0], {}): its physical effect is to write false into v[0]. However, at the source level what’s actually happening is

_Bit_reference br = {};
// and then swap the two _Bit_reference instances:
bool temp = v[0];
v[0] = static_cast<bool>(br);
br = temp;

and the way _Bit_reference::operator bool is implemented is as a dereference-and-mask (see):

return !!(*_M_p & _M_mask);

A default-constructed _Bit_reference has _M_p == nullptr and _M_mask == 0, so we dereference null and then bitwise-AND the result with zero. Both GCC and Clang cleverly observe that anything ANDed with zero is zero, which prevents them from making the even cleverer observation that the null dereference has undefined behavior.


The expressions of the form swap<>(...) rely on a syntax change in C++20, which is already correctly implemented by all of GCC, Clang, and MSVC as far as I can tell.

Prior to C++20, swap< would have been parsed as a function template only if unqualified lookup found a function template named swap in the current scope. Which in this case we have not got. So this expression is simply a parse error in C++17, regardless of what you put in the ... part.

But C++20 (specifically, P0846 “ADL and Function Templates that are not Visible”, subsequently refactored by P1787 “Declarations and where to find them”) changed the rules. Now

A < is interpreted as the delimiter of a template-argument-list if it follows a name that is not a conversion-function-id and

  • that follows the keyword template or a ~ after a nested-name-specifier or in a class member access expression, or
  • for which name lookup finds the injected-class-name of a class template or finds any declaration of a template, or
  • that is an unqualified name for which name lookup either finds one or more functions or finds nothing, or
  • that is a terminal name in a using-declarator, a declarator-id, or a type-only context other than a nested-name-specifier.

Our swap< falls into the third category above: unqualified name lookup on swap finds nothing, so we treat the < as an angle bracket and continue parsing. Of course if you later introduce a variable called swap into the current scope, your call to swap<> will no longer parse; but that’s not too strange in practice.

The C++20 rules do introduce yet another context where it matters what kind of entity is found by name lookup; so we can construct super contrived examples like, say, this:

namespace N {
    enum E { A };
    constexpr struct NTTP {} nttp;
    static bool operator<(int(int), NTTP) { return false; }
    template<NTTP> bool f(E) { return true; }
}

static int f(int x) { return x+1; };

int main() {
    return f<N::nttp>(N::A);  // Is `f` a function?
}

This program returns 1, because the ADL call to N::f<N::nttp> returns true. But if you change the definition of ::f to

static auto f = [](int x) { return x+1; };

then the program returns 0 instead. Because, when f is not a function, f<N::nttp>(N::A) parses as a comparison, equivalent to

return (f < N::nttp) > N::A;

and false > N::A is false.

Posted 2021-06-27