I'm currently trying to get a legacy codebase to build under C++20, and I encountered something like this:
size_t someCount; // value comes from somewhere else
…
std::vector<const char *[2]> keyValues(someCount);
I can't trivially change this to something like std::vector<std:array<const char *, 2>> because this is later passed to some API outside of my control. The above abomination compiles fine with Clang and GCC, and even MSVC as long as I don't enable C++20, but it breaks in MSVC in C++20, as you can see here at Godbolt.
I assume this is related to the DefaultInsertable requirement on T if the above constructor is used (which is indeed the only requirement mandated by the standard). According to cppreference (see previous link), STL implementations up to C++17 used placement new to default-construct the elements, and starting in C++20, std::construct_at is being used for DefaultInsertable types. This may trigger the regression from C++17 to C++20 for MSVC.
The standard says that a type is DefaultInsertable if this expression is well-formed:
allocator_traits<A>::construct(m, p)
So in my case, that would be:
const char * dummy[2];
using Allocator = std::allocator<const char *[2]>;
Allocator a;
// This must be well-formed:
std::allocator_traits<Allocator>::construct(a, dummy);
This compiles fine and without warnings in GCC, Clang and MSVC, so I'll go ahead and assume that const char *[2] is a DefaultInsertable type. But that means that the constructor call in my first example should compile.
Is this an MSVC bug?
The compiler error is:
C:/data/msvc/14.34.31931-Pre/include\xutility(218): error C2440: 'return': cannot convert from 'const char **' to '_Ty (*)'
with
[
_Ty=const char *[2]
]
C:/data/msvc/14.34.31931-Pre/include\xutility(218): note: Types pointed to are unrelated; conversion requires reinterpret_cast, C-style cast or parenthesized function-style cast
C:/data/msvc/14.34.31931-Pre/include\xmemory(673): note: see reference to function template instantiation '_Ty (*std::construct_at<_Objty,,0x0>(_Ty (*const )) noexcept)' being compiled
with
[
_Ty=const char *[2],
_Objty=const char *[2]
]
Before C++20, using array types in std::vector with the default allocator std::allocator was not supported, because std::allocator::destroy would try a simple (pseudo)-destructor call that is ill-formed for anything but class and scalar types. Thereby array types did not satisfy the Erasable requirement with the default allocator, which std::vector requires to be satisfied.
Since C++20 std::allocator is defaulted through std::allocator_traits to use std::destroy_at which in turn has a special case for array types that calls the destructor individually on each element. So Erasable is now satisfied with the default allocator.
However, C++20 also defaulted std::allocator's construct through std::allocator_traits to use std::construct_at. In contrast to the old std::allocator::construct, std::construct_at returns the result of the placement-new expression and has its return-type declared as T* instead of void. T* is also the type of the first argument, i.e. T is the element type.
The issue here is that the placement-new expression inside std::construct_at will not return a pointer-to-array for array types, but instead return a pointer to the first element of the array. If T is an array type U[N], then the return type of the new expression is therefore U*, not U(*)[N] as construct_at is declared to return. Therefore the construct_at instantiation is ill-formed and array types do not satisfy the DefaultInsertable requirement with the default allocator.
It is probably unintended that std::construct_at doesn't work with bounded array types. There is an open LWG issue 3436 for that.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With