Does std::(customization point) invoke the most appropriate overload? - c++

Since C++20, the concept of customization point is introduced in [namespace.std]/7:
Other than in namespace std or in a namespace within namespace std, a program may provide an overload for any library function template designated as a customization point, provided that (a) the overload's declaration depends on at least one user-defined type and (b) the overload meets the standard library requirements for the customization point. [ Note: This permits a (qualified or unqualified) call to the customization point to invoke the most appropriate overload for the given arguments. — end note ]
Does the note part (note the emphasized word "qualified") mean that std::f will automatically invoke the most appropriate overload for f if std::f is a customization point?
A real example is std::swap, which is a designated customization point. Does this mean since C++20, we can write std::swap(a, b) directly instead of using std::swap; swap(a, b);?

A real example is std::swap, which is a designated customization point. Does this mean since C++20, we can write std::swap(a, b) directly instead of using std::swap; swap(a, b);?
No. std::swap itself did not gain any powers. It's still just a function template, so if you call it directly, you're... calling it directly. No ADL or anything.
The point of this is to say how customization points should be opted into. That is, you write:
namespace N { // not std
void swap(Foo&, Foo&);
}
Not:
namespace std {
void swap(N::Foo&, N::Foo&);
}
Nor:
namespace std {
template <>
void swap(N::Foo&, N::Foo&);
}
However, C++20 does introduce a lot of new things called customization point objects which you can use directly do this kind of thing. The CPO for swap is spelled std::ranges::swap (and likewise there are CPOs for all the useful ranges things... ranges::begin, ranges::end, etc.).

Related

Why is deleting a function necessary when you're defining customization point object?

From libstdc++ <concepts> header:
namespace ranges
{
namespace __cust_swap
{
template<typename _Tp> void swap(_Tp&, _Tp&) = delete;
From MS-STL <concepts> header:
namespace ranges {
namespace _Swap {
template <class _Ty>
void swap(_Ty&, _Ty&) = delete;
I've never encountered = delete; outside the context where you want to prohibit the call to copy/move assignment/ctor.
I was curious if this was necessary, so I've commented out the = delete; part from the library like this:
// template<typename _Tp> void swap(_Tp&, _Tp&) = delete;
to see if the following test case compiles.
#include <concepts>
#include <iostream>
struct dummy {
friend void swap(dummy& a, dummy& b) {
std::cout << "ADL" << std::endl;
}
};
int main()
{
int a{};
int b{};
dummy c{};
dummy d{};
std::ranges::swap(a, b);
std::ranges::swap(c, d); // Ok. Prints "ADL" on console.
}
Not only it compiles, it seems to behave well by calling user defined swap for struct dummy. So I'm wondering,
What does template<typename _Tp> void swap(_Tp&, _Tp&) = delete; exactly do in this context?
On what occasion does this break without template<typename _Tp> void swap(_Tp&, _Tp&) = delete;?
TL;DR: It's there to keep from calling std::swap.
This is actually an explicit requirement of the ranges::swap customization point:
S is (void)swap(E1, E2) if E1 or E2 has class or enumeration type ([basic.compound]) and that expression is valid, with overload resolution performed in a context that includes this definition:
template<class T>
void swap(T&, T&) = delete;
So what does this do? To understand the point of this, we have to remember that the ranges namespace is actually the std::ranges namespace. That's important because a lot of stuff lives in the std namespace. Including this, declared in <utility>:
template< class T >
void swap( T& a, T& b );
There's probably a constexpr and noexcept on there somewhere, but that's not relevant for our needs.
std::ranges::swap, as a customization point, has a specific way it wants you to customize it. It wants you to provide a swap function that can be found via argument-dependent lookup. Which means that ranges::swap is going to find your swap function by doing this: swap(E1, E2).
That's fine, except for one problem: std::swap exists. In the pre-C++20 days, one valid way of making a type swappable was to provide a specialization for the std::swap template. So if you called std::swap directly to swap something, your specializations would be picked up and used.
ranges::swap does not want to use those. It has one customization mechanism, and it wants you to very definitely use that mechanism, not template specialization of std::swap.
However, because std::ranges::swap lives in the std namespace, unqualified calls to swap(E1, E2) can find std::swap. To avoid finding and using this overload, it poisons the overload by making visible a version that is = deleted. So if you don't provide an ADL-visible swap for your type, you get a hard error. A proper customization is also required to be more specialized (or more constrained) than the std::swap version, so that it can be considered a better overload match.
Note that ranges::begin/end and similar functions have similar wording to shut down similar problems with similarly named std:: functions.
There were two motivations for the poison pill overloads, most of which don't actually exist anymore but we still have them anyway.
swap / iter_swap
As described in P0370:
The Ranges TS has another customization point problem that N4381 does not cover: an implementation of the Ranges TS needs to co-exist alongside an implementation of the standard library. There’s little benefit to providing customization points with strong semantic constraints if ADL can result in calls to the customization points of the same name in namespace std. For example, consider the definition of the single-type Swappable concept:
namespace std { namespace experimental { namespace ranges { inline namespace v1 {
template <class T>
concept bool Swappable() {
return requires(T&& t, T&& u) {
(void)swap(std::forward<T>(t), std::forward<T>(u));
};
}
}}}}
unqualified name lookup for the name swap could find the unconstrained swap in namespace std either directly - it’s only a couple of hops up the namespace hierarchy - or via ADL if std is an associated namespace of T or U. If std::swap is unconstrained, the concept is “satisfied” for all types, and effectively useless. The Ranges TS deals with this problem by requiring changes to std::swap, a practice which has historically been forbidden for TSs. Applying similar constraints to all of the customization points defined in the TS by modifying the definitions in namespace std is an unsatisfactory solution, if not an altogether untenable.
The Range TS was built on C++14, where std::swap was unconstrained (std::swap didn't become constrained until P0185 was adopted for C++17), so it was important to make sure that Swappable didn't just trivially resolve to true for any type that had std as an associated namespace.
But now std::swap is constrained, so there's no need for the swap poison pill.
However, std::iter_swap is still unconstrained, so there is a need for that poison pill. However, that one could easily become constrained and then we would again have no need for a poison pill.
begin / end
As described in P0970:
For the sake of compatibility with std::begin and ease of migration, std::experimental::ranges::begin accepted rvalues and treated them the same as const lvalues. This behavior was deprecated because it is fundamentally unsound: any iterator returned by such an overload is highly likely to dangle after the full expression that contained the invocation ofbegin
Another problem, and one that until recently seemed unrelated to the design of begin, was that algorithms that return iterators will wrap those iterators in std::experimental::ranges::dangling<>if the range passed to them is an rvalue. This ignores the fact that for some range types — P0789’s subrange<>, in particular — the iterator’s validity does not depend on the range’s lifetime at all. In the case where a prvalue subrange<> is passed to an algorithm, returning a wrapped iterator is totally unnecessary.
[...]
We recognized that by removing the deprecated default support for rvalues from the range access customization points, we made design space for range authors to opt-in to this behavior for their range types, thereby communicating to the algorithms that an iterator can safely outlive its range type. This eliminates the need for dangling when passing an rvalue subrange, an important usage scenario.
The paper went on to propose a design for safe invocation of begin on rvalues as a non-member function that takes, specifically, an rvalue. The existence of the:
template <class T>
void begin(T&&) = delete;
overload:
gives std2::begin the property that, for some rvalue expression E of type T, the expression std2::begin(E) will not compile unless there is a free function begin findable by ADL that specifically accepts rvalues of type T, and that overload is prefered by partial ordering over the general void begin(T&&) “poison pill” overload.
For example, this would allow us to properly reject invoking ranges::begin on an rvalue of type std::vector<int>, even though the non-member std::begin(const C&) would be found by ADL.
But this paper also says:
The author believed that to fix the problem with subrange and dangling would require the addition of a new trait to give the authors of range types a way to say whether its iterators can safely outlive the range. That felt like a hack, and that feeling was reinforced by the author’s inability to pick a name for such a trait that was sufficiently succint and clear.
Since then, this functionality has become checked by a trait - which was first called enable_safe_range (P1858) and is now called enable_borrowed_range (LWG3379). So again, the poison pill here is no longer necessary.

Can iter_swap be specialised?

If I have a container std::vector<T*> items, I can create an IndirectIterator which wraps std::vector<T*>::iterator and allows iterating over T's rather than T*'s.
Can I specialise iter_swap for IndirectIterator to make standard algorithms (such as std::sort) swap items by pointer?
i.e., if I write the following, will it have any effect on standard algorithms?
namespace some_namespace
{
template <typename IterT>
class IndirectIterator
{
IterT m_base;
public:
typedef IterT base_iterator;
typedef /* ... */ reference;
/* ... */
reference operator*() const { **m_base; }
const base_iterator& base() const { return m_base; }
base_iterator& base() { return m_base; }
};
template <typename T>
void iter_swap(IndirectIterator<T>& a, IndirectIterator<T>& b)
{
using std::iter_swap;
iter_swap(a.base(), b.base());
}
}
The benefit of this specialisation is that it swaps pointers rather than full T instances, so it's faster (potentially).
As far as I can see, iter_swap is only used in std::reverse, and it does not mention any kind of argument-dependent lookup: it always uses std::iter_swap. And since you are not allowed to overload functions in the std namespace, you're out of luck.
Can I specialise iter_swap for IndirectIterator to make standard
algorithms (such as std::sort) swap items by pointer?
You can always make your overload/specialization. However your question is whether you can specialize iter_swap inside the namespace std.
I think the answer is unclear from the standard.
I have found that in some cases, I had to define a special iter_swap inside std so that std::sort uses it. In gcc std lib std::sort uses the qualified std::iter_swap.
This is probably a defect in std::sort.
IMO std::sort should call an unqualified swap_iter.
i.e., if I write the following, will it have any effect on standard
algorithms?
Not in GCC (at least) because standard algorithms use qualified std::iter_swap (bug?)
I don't think the standard is clear about it.
You are allowed to reopen std namespace and specialize templates inside std as long as you specialize them for user-defined types. In your case you can actually specialize std::iter_swap for your purposes, just make sure you are doing it in std namespace, not in your own namespace (as in your example). It is not very elegant, but it is allowed.
Starting in C++20, the answer is unambiguously "yes, you can customize your own ADL iter_swap." STL algorithms should use it, but aren't required to, and there is implementation divergence in practice.
For customizing a customization point, you should use the "hidden friend idiom":
namespace some_namespace {
template <class IterT>
class IndirectIterator {
IterT m_base;
public:
/* ... */
reference operator*() const { return **m_base; }
const base_iterator& base() const { return m_base; }
// "It's not a member, it's just a friend"
friend void iter_swap(IndirectIterator a, IndirectIterator b) {
using std::iter_swap;
iter_swap(a.base(), b.base());
}
};
}
Subtle change here: your iter_swap had been taking its arguments by mutable reference! That wouldn't work, e.g. if it were passed an IndirectIterator that was const, or that was an rvalue. Iterator parameters should always be taken by value. Similarly, there's no sense in providing a non-const version of base(). Your base() is a getter, not a setter; it should be a const member function for the same reason vector::size() is a const member function.
Finally, the big caveat here is that you must never customize iter_swap to do anything observably different from swapping the targets of the iterators. If you do that, then it's undefined behavior — all bets are off — the program could do anything. Formally, this requirement is given in [iterator.cust.swap]:
If the [iter_swap] function selected by overload resolution does not exchange the values denoted by E1 and E2, the program is ill-formed, no diagnostic required.
I believe your idea of having iter_swap swap the "first-level" targets rather than the "second-level" targets is perfectly kosher, as long as you're never going to rely on observing the difference. The standard library is still allowed to bypass iter_swap and just do swap(*it, *kt) if it feels like it; your code must be able to cope with that, or else "the program is ill-formed, no diagnostic required." (I.e., you must fully commit to the idea that swapping those first-level targets is tantamount to "exchanging the values of E1 and E2," and thus interchangeable with any other method of exchanging the values. You can't rely on your own method getting used.)
Here is a complete C++20 example on Godbolt. As of February 2023, here's where I see the customized iter_swap actually getting used:
Algorithm
libstdc++
libc++
Microsoft
std::ranges::partition
Yes
Yes
Yes
std::ranges::sort
No
No
Yes
std::partition
No
No
No
std::sort
No
No
No

How to implement swap()? [duplicate]

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
how to provide a swap function for my class?
Every time I think I understand it, I see something that really confuses me.
If you want to provide an implementation of swap for your own class, what do you do?
The list of possibilities is:
Define one inside the std namespace (taking two arguments), which calls #3 below
(some say this is correct; some people say illegal)
Define a static method inside the class, taking two arguments to swap
(makes more sense to me than #4, but I don't get why no one does this), which calls any base-class swaps as necessary
Define an instance method inside the class, taking one other argument to swap with, which calls any base-class swaps as necessary
Define an instance method inside the class, taking two other arguments to swap, also here, which calls any base-class swaps as necessary
Define one in your own namespace (taking two arguments), which calls #3
Something else
My own understanding is that I need #5 and #3, where the caller would then be calling swap likeusing std::swap; swap(a, b);,but the fact that no one seems to suggest that combination is really confusing me. And I really don't understand #4 at all, because everyone seems to be using an instance member when in fact the operation is static. I can't tell if my understanding is wrong or a bunch of the answers I see when looking this up.
What's the correct way?
A common pattern I have seen is providing 3 and 5, as per your own understanding.
adds an specialization to the std:: namespace, which is allowed, but might not be possible in all cases (if your type is a template itself).
offers no advantage at all, and forces qualifying with the type when used outside of one of the members, which means that for implementing swap on other types that hold your type as a member, they will need to qualify the call (void swap( other& l, other& r ) { T::swap( l.t, r.t ); })
does not need friendship, allows for use with rvalues (even in C++03) and is idiomatic in some cases std::vector<int>().swap( v ); to clear the contents of the vector.
What? You misunderstood the code! That is not declaring a member taking two arguments, but rather a free function taking the two arguments, and defines the function inline. This is equivalent to 5 (without forwarding to 3, but rather implementing everything in the free function).
Free function in the same namespace allows for ADL to find it, and enables other code to use the common pattern of void swap( other& l, other& r ) { using std::swap; swap( l.t, r.t ); } without having to know whether the type of other::t has an specific swap overload or the one in std:: needs to be used. Forwarding to 3 allows you to provide a single (real) implementation that can be used through ADL and also on temporary objects.
The C++11 standard says an object t is swappable with an object u if swap(t, u) and swap(u, t) are valid expressions that select non-member functions called swap from an overload set that includes the two std::swap templates in <utility> and any overloads found by Argument Dependent Lookup, such that the values of t and u are exchanged.
The conventional way to swap two objects under the conditions described above is:
using std::swap;
swap(t, u);
Here name lookup will consider std::swap and any overloads of swap found by ADL, then overload resolution will pick the best match.
Considering each of your implementations:
It is not legal to add overloads to namespace std, and the rule above doesn't require them to be found anyway. It is legal to specialize the standard std::swap function templates if the specialization is for a user-defined type (i.e. not a standard library type or fundamental type.) If you specialize std::swap it's valid, otherwise not, and you can't partially-specialize function templates.
A static member function will not be found by swap(t, u) because it needs to be qualified e.g. Foo::swap(t, u)
A unary swap member function cannot be called as swap(t, u), it has to be called as t.swap(u)
The links you give show a non-member friend function, which can be found by ADL, so can be used to make types swappable.
Such a function can be found by ADL.
So 2 and 3 do not make a type swappable. 4 does, and 5 does. 1 might do, if done correctly, but cannot be used for swapping class templates.
3 is not required, but can be used to help implement either 4 or 5, because a member function will have access to the type's internal details so can swap private members. For 4 the function is a friend, so has access already. So by a process of elimination you need either 4 or 3 and 5. It is generally considered better to provide a member swap (3) and then provide a non-member function in the same namespace (5) which calls the member swap.
As you say, you need #5 (a function in the same namespace as your type) to support the idiomatic using std::swap; swap(a,b);. That way, your overload will be selected by argument-dependent lookup in preference to std::swap.
If your implementation of swap needs to access the type's privates, then you will either need to call a member function like #3, or declare the non-member function a friend. This is what your examples in #4 do: a friend function can be declared and defined inside the class, but that does not make it a member; it is still scoped within the surrounding namespace.
So this:
class thing {
friend void swap(thing & a, thing & b) {/*whatever*/}
};
is equivalent to this (more or less - see comments):
class thing {
friend void swap(thing & a, thing & b);
};
inline void swap(thing & a, thing & b) {/*whatever*/}
#1 (specialising std::swap for your type) is allowed, but some would regard it as cleaner to keep everything in your own namespace.
Neither #2 nor #3 would allow an unqualified swap(a,b) to find your implementation.

Rationale for Koenig lookup

What's the rationale of Koenig lookup?
Cannot avoid thinking of it like something that makes your code a lot harder to read and more instable.
Couldn't they define Koenig lookup so that it only work for specific cases (ie: non-member operators) or when explicitly required?
The original motivation, IIRC, was to be able to write
std::cout << 42;
without having to qualify std::operator<<(std::ostream&, int) explicitely.
If you want to disable argument dependant lookup, you can explicitely qualify the function name, ie. use std::swap instead of swap to prevent swap to be looked up in whatever namespace its arguments would live.
ADL can also be used with SFINAE to test at compile time whether some function is defined for a particular type (I'll let you work this out as an exercise, there is at least one question about this on Stackoverflow).
The strongest use case for ADL is for cases like this.
namespace A
{
struct S {};
S operator+( const S&, const S& );
}
namespace B
{
A::S test()
{
A::S a, b;
return a + b;
}
}
It is also useful for selecting the correct swap function in generic code so it shouldn't only apply to operator functions. It is already a fairly complex part of the standard, making rules that prevented it from working in some cases would add further complexity, what would be the gain?
I can't think of any neat way of asking for it explicitly that would be significantly less verbose than calling a function in a different namespace directly and would, in any case, make expressions more complex.
We're you thinking something like: return [[ use_adl ]] (a + b); vs. return A::operator+( a, b ); ?

What are the pitfalls of ADL?

Some time ago I read an article that explained several pitfalls of argument dependent lookup, but I cannot find it anymore. It was about gaining access to things that you should not have access to or something like that. So I thought I'd ask here: what are the pitfalls of ADL?
There is a huge problem with argument-dependent lookup. Consider, for example, the following utility:
#include <iostream>
namespace utility
{
template <typename T>
void print(T x)
{
std::cout << x << std::endl;
}
template <typename T>
void print_n(T x, unsigned n)
{
for (unsigned i = 0; i < n; ++i)
print(x);
}
}
It's simple enough, right? We can call print_n() and pass it any object and it will call print to print the object n times.
Actually, it turns out that if we only look at this code, we have absolutely no idea what function will be called by print_n. It might be the print function template given here, but it might not be. Why? Argument-dependent lookup.
As an example, let's say you have written a class to represent a unicorn. For some reason, you've also defined a function named print (what a coincidence!) that just causes the program to crash by writing to a dereferenced null pointer (who knows why you did this; that's not important):
namespace my_stuff
{
struct unicorn { /* unicorn stuff goes here */ };
std::ostream& operator<<(std::ostream& os, unicorn x) { return os; }
// Don't ever call this! It just crashes! I don't know why I wrote it!
void print(unicorn) { *(int*)0 = 42; }
}
Next, you write a little program that creates a unicorn and prints it four times:
int main()
{
my_stuff::unicorn x;
utility::print_n(x, 4);
}
You compile this program, run it, and... it crashes. "What?! No way," you say: "I just called print_n, which calls the print function to print the unicorn four times!" Yes, that's true, but it hasn't called the print function you expected it to call. It's called my_stuff::print.
Why is my_stuff::print selected? During name lookup, the compiler sees that the argument to the call to print is of type unicorn, which is a class type that is declared in the namespace my_stuff.
Because of argument-dependent lookup, the compiler includes this namespace in its search for candidate functions named print. It finds my_stuff::print, which is then selected as the best viable candidate during overload resolution: no conversion is required to call either of the candidate print functions and nontemplate functions are preferred to function templates, so the nontemplate function my_stuff::print is the best match.
(If you don't believe this, you can compile the code in this question as-is and see ADL in action.)
Yes, argument-dependent lookup is an important feature of C++. It is essentially required to achieve the desired behavior of some language features like overloaded operators (consider the streams library). That said, it's also very, very flawed and can lead to really ugly problems. There have been several proposals to fix argument-dependent lookup, but none of them have been accepted by the C++ standards committee.
The accepted answer is simply wrong - this is not a bug of ADL. It shows an careless anti-pattern to use function calls in daily coding - ignorance of dependent names and relying on unqualified function names blindly.
In short, if you are using unqualified name in the postfix-expression of a function call, you should have acknowledged that you have granted the ability that the function can be "overridden" elsewhere (yes, this is a kind of static polymorphism). Thus, the spelling of the unqualified name of a function in C++ is exactly a part of the interface.
In the case of the accepted answer, if the print_n really need ADL print (i.e. allowing it to be overridden), it should have been documented with the use of unqualified print as an explicit notice, thus clients would receive a contract that print should be carefully declared and the misbehavior would be all of the responsibility of my_stuff. Otherwise, it is a bug of print_n. The fix is simple: qualify print with prefix utility::. This is indeed a bug of print_n, but hardly a bug of the ADL rules in the language.
However, there do exist unwanted things in the language specification, and technically, not only one. They are realized more than 10 years, but nothing in the language is fixed yet. They are missed by the accepted answer (except that the last paragraph is solely correct till now). See this paper for details.
I can append one real case against the name lookup nasty. I was implementing is_nothrow_swappable where __cplusplus < 201703L. I found it impossible to rely on ADL to implementing such feature once I have a declared swap function template in my namespace. Such swap would always found together with std::swap introduced by a idiomatic using std::swap; to use ADL under the ADL rules, and then there would come ambiguity of swap where the swap template (which would instantiate is_nothrow_swappable to get the proper noexcept-specification) is called. Combined with 2-phase lookup rules, the order of declarations does not count, once the library header containing the swap template is included. So, unless I overload all my library types with specialized swap function (to supress any candidate generic templates swap being matched by overloading resolution after ADL), I cannot declare the template. Ironically, the swap template declared in my namespace is exactly to utilize ADL (consider boost::swap) and it is one of the most significant direct client of is_nothrow_swappable in my library (BTW, boost::swap does not respects the exception specification). This perfectly beat my purpose up, sigh...
#include <type_traits>
#include <utility>
#include <memory>
#include <iterator>
namespace my
{
#define USE_MY_SWAP_TEMPLATE true
#define HEY_I_HAVE_SWAP_IN_MY_LIBRARY_EVERYWHERE false
namespace details
{
using ::std::swap;
template<typename T>
struct is_nothrow_swappable
: std::integral_constant<bool, noexcept(swap(::std::declval<T&>(), ::std::declval<T&>()))>
{};
} // namespace details
using details::is_nothrow_swappable;
#if USE_MY_SWAP_TEMPLATE
template<typename T>
void
swap(T& x, T& y) noexcept(is_nothrow_swappable<T>::value)
{
// XXX: Nasty but clever hack?
std::iter_swap(std::addressof(x), std::addressof(y));
}
#endif
class C
{};
// Why I declared 'swap' above if I can accept to declare 'swap' for EVERY type in my library?
#if !USE_MY_SWAP_TEMPLATE || HEY_I_HAVE_SWAP_IN_MY_LIBRARY_EVERYWHERE
void
swap(C&, C&) noexcept
{}
#endif
} // namespace my
int
main()
{
my::C a, b;
#if USE_MY_SWAP_TEMPLATE
my::swap(a, b); // Even no ADL here...
#else
using std::swap; // This merely works, but repeating this EVERYWHERE is not attractive at all... and error-prone.
swap(a, b); // ADL rocks?
#endif
}
Try https://wandbox.org/permlink/4pcqdx0yYnhhrASi and turn USE_MY_SWAP_TEMPLATE to true to see the ambiguity.
Update 2018-11-05:
Aha, I am bitten by ADL this morning again. This time it even has nothing to do with function calls!
Today I am finishing the work of porting ISO C++17 std::polymorphic_allocator to my codebase. Since some container class templates have been introduced long ago in my code (like this), this time I just replace the declarations with alias templates like:
namespace pmr = ystdex::pmr;
template<typename _tKey, typename _tMapped, typename _fComp
= ystdex::less<_tKey>, class _tAlloc
= pmr::polymorphic_allocator<std::pair<const _tKey, _tMapped>>>
using multimap = std::multimap<_tKey, _tMapped, _fComp, _tAlloc>;
... so it can use my implementation of polymorphic_allocator by default. (Disclaimer: it has some known bugs. Fixes of the bugs would be committed in a few days.)
But it suddenly does not work, with hundreds of lines of cryptic error messages...
The error begins from this line. It roughly complains that the declared BaseType is not a base of the enclosing class MessageQueue. That seems very strange because the alias is declared with exactly the same tokens to those in the base-specifier-list of the class definition, and I am sure nothing of them can be macro-expanded. So why?
The answer is... ADL sucks. The line inroducing BaseType is hard-coded with a std name as a template argument, so the template would be looked up per ADL rules in the class scope. Thus, it finds std::multimap, which differs to the result of lookup in as the actual base class declared in the enclosing namespace scope. Since std::multimap uses std::allocator instance as the default template argument, BaseType is not the same type to the actual base class which have an instance of polymorphic_allocator, even multimap declared in the enclosing namespace is redirected to std::multimap. By adding the enclosing qualification as the prefix right to the =, the bug is fixed.
I'd admit I am lucky enough. The error messages are heading the problem to this line. There are only 2 similar problems and the other is without any explicit std (where string is my own one being adapted to ISO C++17's string_view change, not std one in pre-C++17 modes). I would not figure out the bug is about ADL so quickly.