Description
tl;dr:
clang, starting from version 7.0.1 all the way up to the current trunk, in C++17 mode fails to compile the following code when combined with libstdc++ version <12:
https://godbolt.org/z/vrKc9877v
(click/tap to expand, this is the stripped minimal example that I could come up with that reproduces the problem)
#include <vector>
#include <unordered_map>
#include <variant>
#include <type_traits>
#include <cstdint> // for uint8_t
namespace detail {
template<typename Allocator, typename T>
using ReboundAllocator = typename std::allocator_traits<Allocator>::template rebind_alloc<T>;
template<typename T>
using RemoveCVRef = std::remove_cv_t<std::remove_reference_t<T>>;
}
template<typename Allocator>
struct BasicValue {
using Null = std::monostate;
using Object = std::unordered_map<int, BasicValue,
std::hash<int>, std::equal_to<int>,
detail::ReboundAllocator<Allocator, std::pair<const int, BasicValue>>>;
using Variant = std::variant<Null, Object>;
BasicValue() = default;
template<typename T,
std::enable_if_t<!std::is_same_v<detail::RemoveCVRef<T>, BasicValue> &&
std::is_convertible_v<T&&, Variant>, int> = 0>
BasicValue(T &&v) noexcept(std::is_nothrow_constructible_v<Variant, T&&>) : m_data{ std::forward<T>(v) } {}
private:
struct Workaround final {
Workaround() noexcept { ::new(&storage) Variant{}; }
template<typename... T>
Workaround(T&&... v) noexcept(std::is_nothrow_constructible_v<Variant, T&&...>) {
::new(&storage) Variant{ std::forward<T>(v)... };
}
Workaround(const Workaround &other) { ::new(&storage) Variant{ other.operator const Variant & () }; }
~Workaround() {
static_assert(sizeof(DummyVariant) == sizeof(Variant));
static_assert(alignof(DummyVariant) == alignof(Variant));
operator Variant&().~Variant();
}
auto &operator=(const Workaround &other) { return operator Variant & () = other.operator const Variant & (); }
operator const Variant&() const& noexcept { return *std::launder(reinterpret_cast<const Variant*>(&storage)); }
operator Variant&() & noexcept { return *std::launder(reinterpret_cast<Variant*>(&storage)); }
private:
using DummyUnorderedMap = std::unordered_map<int, int,
std::hash<int>, std::equal_to<int>,
detail::ReboundAllocator<Allocator, std::pair<const int, int>>>;
using DummyVariant = std::variant<Null, DummyUnorderedMap>;
alignas(DummyVariant) uint8_t storage[sizeof(DummyVariant)]{};
} m_data;
};
// for some reason clang needs this
//static_assert(std::is_copy_constructible_v<BasicValue<std::allocator<char>>>);
using Value = BasicValue<std::allocator<char>>;
struct ShouldBeIrrelevant {
// commenting any of the following lines gives:
// "error: incomplete type 'std::unordered_map<...>"
Value value;
std::vector<int> bar;
};
int main() {
Value v;
const auto c = std::unordered_map<int, Value>{};
//error: call to implicitly-deleted copy constructor of 'std::pair<const int, BasicValue<std::allocator<char>>>'
v = c;
}
error: call to implicitly-deleted copy constructor of 'std::pair<const int, BasicValue<std::allocator<char>>>'
while other compilers have no problems with this code, namely GCC and MSVC.
Problem
C++17 mode
Something breaks in clang (practically all versions supporting C++17 "enough", i.e. >7.0.1) with this code when it is used with libstdc++ version <12 (in particular versions 9.2..11.4, earlier versions do not support C++17 "enough").
If you remove the part
struct ShouldBeIrrelevant {
Value value;
std::vector<int> bar;
};
or one or both of the members clang fails with error:
error: incomplete type 'std::unordered_map<int, BasicValue<std::allocator<char>>>' used in type trait expression
though it seems completely irrelevant to the code that causes the error:
Value v;
const auto c = std::unordered_map<int, Value>{};
v = c; // error
Later standard modes
With e.g. C++20 or C++23, the error is spelled differently but is practically the same:
error: no matching function for call to 'construct_at'
Workaround
If I add a static_assert
static_assert(std::is_copy_constructible_v<BasicValue<std::allocator<char>>>);
everythng starts to work as expected, and the struct ShouldBeIrrelevant
snippet becomes truly irrelevant, i.e. removing it completely or removing any or both of the members has no effect.
Context
In libstdc++ versions <12 std::unordered_map
does not support incomplete types, we can work around that by using byte array storage, placement ::new()
to construct an object in that storage, and reinterpret_cast
to turn it to a pointer to desired type inside implementation where the type is complete.
We guess that alignment and size of some complete std::unordered_map
instantiation is the same as the one with currently incomplete type, and static_assert
that we guessed correctly in the destructor.