diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt index 5bd8337..8a8824c 100644 --- a/benchmark/CMakeLists.txt +++ b/benchmark/CMakeLists.txt @@ -15,15 +15,9 @@ if(CMAKE_BUILD_TYPE MATCHES "Release|RelWithDebInfo") endif() endif() -# Macro to set properties for Xcode targets macro(set_xcode_properties TARGET_NAME) - if(CMAKE_GENERATOR STREQUAL Xcode) - set_target_properties(${TARGET_NAME} PROPERTIES - XCODE_ATTRIBUTE_ENABLE_AVX YES - XCODE_ATTRIBUTE_ENABLE_AVX2 YES - XCODE_ATTRIBUTE_OTHER_CPLUSPLUSFLAGS "-mavx -mavx2 -mbmi2" - XCODE_ATTRIBUTE_OTHER_CFLAGS "-mavx -mavx2 -mbmi2" - ) + if(CMAKE_GENERATOR STREQUAL Xcode AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64") + target_compile_options(${TARGET_NAME} PRIVATE -mavx -mavx2 -mbmi2) endif() endmacro() @@ -73,3 +67,39 @@ target_link_libraries( zoo-c_str-implementations benchmark::benchmark ) + +# Generated by ChatGPT o1-preview +add_executable( + catch2StringTest + main.catch2.cpp string.catch2.cpp +) + +set_xcode_properties(catch2StringTest) + +target_include_directories( + catch2StringTest PRIVATE + str-experiment + ../test/inc + ../inc + ../junkyard/inc + ../test/third_party/Catch2/single_include +) + + +# not yet mergeable with the master branch +add_executable( + str-benchmark.catch2 + onlyMain.benchmark.catch2.cpp + str.benchmark.catch2.cpp +) + +set_xcode_properties(str-benchmark.catch2) + +target_include_directories( + str-benchmark.catch2 PRIVATE + str-experiment + ../test/inc + ../inc + ../junkyard/inc + ../test/third_party/Catch2/single_include +) diff --git a/benchmark/main.catch2.cpp b/benchmark/main.catch2.cpp new file mode 100644 index 0000000..f481b16 --- /dev/null +++ b/benchmark/main.catch2.cpp @@ -0,0 +1,3 @@ +#define CATCH_CONFIG_MAIN + +#include diff --git a/benchmark/onlyMain.benchmark.catch2.cpp b/benchmark/onlyMain.benchmark.catch2.cpp new file mode 100644 index 0000000..c21dd95 --- /dev/null +++ b/benchmark/onlyMain.benchmark.catch2.cpp @@ -0,0 +1,6 @@ +#define CATCH_CONFIG_ENABLE_BENCHMARKING +#define CATCH_CONFIG_MAIN + +#include + + diff --git a/benchmark/str-experiment.src/Heap.cpp b/benchmark/str-experiment.src/Heap.cpp new file mode 100644 index 0000000..e7d113c --- /dev/null +++ b/benchmark/str-experiment.src/Heap.cpp @@ -0,0 +1,8 @@ +// +// Heap.cpp +// str-benchmark.catch2 +// +// Created by Eduardo Madrid on 12/3/24. +// + +#include diff --git a/benchmark/str-experiment/zoo/Str.h b/benchmark/str-experiment/zoo/Str.h new file mode 100644 index 0000000..81075c7 --- /dev/null +++ b/benchmark/str-experiment/zoo/Str.h @@ -0,0 +1,289 @@ +#include +#include // memcpy +#include // aligned allocations +//#include +#include + +namespace zoo { + +template +constexpr auto Log2Floor(T value) { + return sizeof(unsigned long long) * 8 - 1 - + //std::countl_zero(value); + __builtin_clzll(value); +} + +template +// assumes 1 < value! +constexpr auto Log2Ceiling(T value) { + return 1 + Log2Floor(value - 1); +} + +template +constexpr auto Log256Celing(T value) { + return (7 + Log2Ceiling(value)) / 8; +} + +inline void noOp(...) {} +#define printf noOp +#define fprintf noOp + +/// \brief String controller type optimized for minimum +/// size and configurability +/// +/// Design: +/// This type attempts to use a minimum size fully usable string type +/// compatible with the API of std::string. The extreme case is when +/// sizeof(Str) == sizeof(void *). This type is a template that +/// takes a type argument to serve as the *prototype* for the local +/// buffer. If you select "void *" as the prototype, the local buffer and +/// the size of Str will have size and alignment of void *. If you +/// select "void *[2]" (array of two pointers), then there will be 2 void +/// pointers worth of space (16 bytes) locally. +/// +/// To maximize space efficiency, the encoding of whether the string is +/// local or on the heap will occur at the last byte, least significant +/// bits of the local storage. The space corresponding to the last pointer +/// worth of local storage doubles as the encoding of a heap pointer for +/// strings that cannot be held locally. +/// +/// This uses Andrei Alexandrescu's idea of storing the size of the string +/// as the remaining local capacity. In this way, a 0 local capacity also +/// encodes the null byte for c_str(), as reported by Nicholas Omrod at +/// CPPCon 2016. In case the string is too long for local allocation, the +/// pointer in the heap will be encoded locally in the last word of the +/// representation. +/// +/// For an application expecting large strings, the optimal size of the +/// local storage would be that of a "char *". Since most strings would be +/// large, there would be few chances of storing them in the local buffer. +/// You could use "char *" or "void *" for the storage prototype. +/// +/// \tparam StorageModel Indicates the size of the local storage and its +/// alignment. +/// +/// For large strings (allocated in the heap): +/// In the last void * worth of local storage: +/// +---------------- Big Endian diagram -----------------------------+ +/// | Most significant part of Pointer | Last Byte, rob 4 lower bits | +/// +-----------------------------------------------------------------+ +/// Since we're robbing 4 bits from the last byte, this implies the pointers +/// are rounded up to 16 bytes. +/// +/// For local strings: +/// +------- Little Endian Diagram -----+ +/// | characters... | last byte | +/// +-----------------------------------+ +/// +/// The last byte looks like this: +/// For heap encoding: the number of bytes required to encode the size of +/// the string, or mathematically, the logarithm base 256 of the string's +/// size. This value will be in the interval [1, 8], encoded with a shifted +/// range of [0, 7], allowing us to use 3 bits to encode the length. An +/// extra bit flags whether the local buffer stores a pointer to a heap +/// string (1) or if it is represented in the local buffer (0). +/// +/// Because the storage model can be of arbitrary size, we cannot assume +/// the lowest available bit will encode 8. It will be the lowest bit the +/// local buffer size allows. For a storage model of void *[4], the heap +/// flag encodes for 32 as a number. For Str, the bit encodes for 8. +/// This strategy forces allocations to align to the ceiling of +/// log2(sizeof(StorageModel)). +/// +/// Local strings: +/// The last byte serves as the COUNT OF REMAINING BYTES, including the null +/// character terminator. Hence, Str can hold up to seven bytes +/// + null terminator locally. Str allows 8*8 = 64 characters +/// locally (including the null). +/// +/// Heap-allocated strings: +/// As explained, the lower three bits indicate the number of bytes needed +/// to encode the string's length, shifted by 1. The memory referenced by +/// the encoded pointer starts with however many bytes are needed to encode +/// the length, followed by the actual bytes of the string. +/// +/// The Case For A Custom Allocator: +/// A custom allocator is considered for integration with the StorageModel +/// for Str. The considerations are: +/// * The size of the StorageModel can be set to the size of char *, the +/// theoretical optimal size of the StorageModel. +/// * The remaining sizes can be divided into arenas (with increasing limit +/// sizes) to minimize waste. Regular (constexpr) steps (e.g., 1 between +/// 8-15, 2 between 16-23, etc.) can be trialed. +/// * An arena should 'steal' a slot from an arena whose base size is a +/// multiple of the original. This forces us to maintain metadata on the +/// size and load factor for each arena. +/// * The previous point aims to minimize hits on the general allocator. +template +struct Str { + constexpr static auto + Size = sizeof(StorageModel), // remaining count is Size - used + BitsToEncodeLocalLength = Log2Ceiling(Size - 1), + CodeSize = BitsToEncodeLocalLength + 1, + HeapAlignmentRequirement = std::size_t(1) << CodeSize; + ; + constexpr static char OnHeapIndicatorFlag = 1 << BitsToEncodeLocalLength; + static_assert(sizeof(void *) <= Size); + alignas(alignof(StorageModel)) char buffer_[sizeof(StorageModel)]; + + auto lastByte() const noexcept { return buffer_[Size - 1]; } + auto &lastByte() noexcept { return buffer_[Size - 1]; } + auto codePtr() noexcept { return reinterpret_cast(buffer_ + Size - sizeof(char *)); } + const auto codePtr() const noexcept { return const_cast(this)->codePtr(); } + + auto allocationPtr() const noexcept { + uintptr_t codeThatContainsAPointer; + memcpy(&codeThatContainsAPointer, codePtr(), sizeof(char *)); + assert(onHeap()); + auto asLittleEndian = __builtin_bswap64(codeThatContainsAPointer); + auto bytesForSizeMinus1 = asLittleEndian & 7; + auto codeRemoved = + asLittleEndian ^ OnHeapIndicatorFlag ^ bytesForSizeMinus1; + + char *rv; + memcpy(&rv, &codeRemoved, sizeof(char *)); + return rv; + } + + auto encodePointer(char numberOfBytes, const char *ptr) { + uintptr_t code; + memcpy(&code, &ptr, sizeof(char *)); + static_assert(8 == sizeof(char *)); + if(8 < numberOfBytes) { + __builtin_unreachable(); + } + return + __builtin_bswap64(code | (numberOfBytes - 1) | OnHeapIndicatorFlag); + } + + auto size() const noexcept { + if(onHeap()) { + auto countOfBytesForEncodingSize = 1 + (lastByte() & 7); + const char *ptr = allocationPtr(); + std::remove_const_t length = 0; + memcpy(&length, ptr, countOfBytesForEncodingSize); + return length - 1; + } + else { + return Size - lastByte() - 1; + } + } + + auto onHeap() const noexcept { + return bool(OnHeapIndicatorFlag & lastByte()); + } + + Str(const char *source, std::size_t length) { + if(length <= Size) { + lastByte() = Size - length; + memcpy(buffer_, source, length); + } else { + auto bytesForSize = (Log2Ceiling(length) + 7) >> 3; + auto onHeap = + new(std::align_val_t(HeapAlignmentRequirement)) + char[bytesForSize + length]; + fprintf(stderr, "onHeap: %p\n", onHeap); + + assert(onHeap); + // assumes little endian here! + memcpy(onHeap, &length, bytesForSize); + memcpy(onHeap + bytesForSize, source, length); + + auto code = encodePointer(bytesForSize, onHeap); + auto addressOfLastPointerWorthOfSpace = codePtr(); + memcpy(addressOfLastPointerWorthOfSpace, &code, sizeof(char *)); + // We encode the range [1..8] as [0..7] + + printf("buffer_ : %p:\n", buffer_); + for (auto i = 0; i < Size; i++) { + printf("%02x ", (unsigned char)buffer_[i]); + } + printf("\n"); + auto ptrToStr = allocationPtr() + bytesForSize; + printf("%p \n", ptrToStr); + printf("\n"); + + for (auto i = 0; i <= length; i++) { + printf("%c ", (unsigned char)ptrToStr[i]); + } + printf("\n"); + + printf("Size: %lu\n", Size); + printf("BitsToEncodeLocalLength: %lu\n", BitsToEncodeLocalLength); + printf("CodeSize: %lu\n", CodeSize); + printf("HeapAlignmentRequirement: %lu\n", HeapAlignmentRequirement); + } + } + + template + Str(const char (&in)[L]) noexcept(L <= Size): + Str(static_cast(in), L - 1) + {} + + Str() noexcept { + buffer_[0] = '\0'; + lastByte() = Size - 1; + } + + Str(const Str &model): Str(model.c_str(), model.size() + 1) {} + + Str(Str &&donor) noexcept { + memcpy(buffer_, donor.buffer_, sizeof(this->buffer_)); + donor.lastByte() = ~OnHeapIndicatorFlag; + } + + Str &operator=(const Str &model) { + if(!model.onHeap()) { + if(onHeap()) { delete[] allocationPtr(); } + else { + if(this == &model) { return *this; } + } + new(this) Str(model); + return *this; + } + // The model is on the heap + auto modelSize = model.size(); + if(size() < modelSize) { + if(onHeap()) { this->~Str(); } + new(this) Str(model); + } else { + if(this == &model) { return *this; } + // assert we are on the heap too + auto modelBytesForSize = 1 + (model.lastByte() & 7); + auto myPtr = allocationPtr(); + auto modelPtr = model.allocationPtr(); + memcpy(myPtr, modelPtr, modelBytesForSize + modelSize + 1); + encodePointer(modelBytesForSize, myPtr); + } + return *this; + }; + + Str &operator=(Str &&donor) noexcept { + Str temporary(std::move(donor)); + this->~Str(); + return *new(this) Str(std::move(temporary)); + } + + ~Str() { + if(onHeap()) { delete[] allocationPtr(); } + } + + const char *c_str() const noexcept { + if (!onHeap()) { + return buffer_; + } + + // the three least-significant bits of code contain the size of length, + // encoded as a range [0..7] that maps to a range [1..8] in which each + // the unit is a byte + auto byteWithEncoding = lastByte(); + auto bytesForLength = 1 + (byteWithEncoding & 7); + + auto location = allocationPtr(); + auto rv = location + bytesForLength; + fprintf(stderr, "Returning %s\n", rv); + return rv; + } +}; + +} // closes zoo diff --git a/benchmark/str.benchmark.catch2.cpp b/benchmark/str.benchmark.catch2.cpp new file mode 100644 index 0000000..5b6f569 --- /dev/null +++ b/benchmark/str.benchmark.catch2.cpp @@ -0,0 +1,273 @@ +#include "zoo/Str.h" + +#define CATCH_CONFIG_ENABLE_BENCHMARKING +#include + +#include +#include +#include +#include +#include + +auto loadTextCorpus(std::string_view path) { + std::vector strings; + std::vector ints; + std::unordered_map forwardMap; + int nextIndex = 0; + + std::ifstream text{path}; + if(!text) { abort(); } + std::string line; + std::regex separator{"\\w+"}; + text.clear(); + text.seekg(0); + while(text) { + getline(text, line); + std::sregex_iterator + wordsEnd{}, + wordIterator{line.begin(), line.end(), separator}; + while(wordsEnd != wordIterator) { + const auto &word = wordIterator->str(); + strings.push_back(word); + auto [where, notPresent] = + forwardMap.insert({ word, nextIndex }); + ints.push_back( + notPresent ? nextIndex++ : where->second + ); + ++wordIterator; + } + } + WARN(forwardMap.size() << " different strings identified"); + REQUIRE(ints.size() == strings.size()); + REQUIRE(forwardMap.size() == nextIndex); + return std::tuple(strings, ints); +} + +template +auto allocationPtr(const STR &c) { + return c.c_str(); +} +template +auto allocationPtr(const zoo::Str &c) { + return c.allocationPtr(); +} + +template +auto onHeap(const STR &c) { + auto cStr = c.c_str(); + uintptr_t asInteger; + memcpy(&asInteger, &cStr, sizeof(char *)); + constexpr auto StringTypeSize = sizeof(STR); + auto baseAddress = &c; + uintptr_t baseAddressAsInteger; + memcpy(&baseAddressAsInteger, &baseAddress, sizeof(char *)); + if( + baseAddressAsInteger <= asInteger && + asInteger < baseAddressAsInteger + StringTypeSize + ) { return false; } + return true; +} + +template +auto onHeap(const zoo::Str &c) { + return c.onHeap(); +} + +template +struct IsZooStr: std::false_type {}; +template +struct IsZooStr>: std::true_type {}; + +static_assert(IsZooStr>::value); +static_assert(!IsZooStr::value); + +template +auto minimumUsedBytes(const STR &s) { + if(!onHeap(s)) { + return sizeof(STR); + } + auto where = allocationPtr(s); + uintptr_t asInt; + memcpy(&asInt, &where, sizeof(char *)); + auto trailingZeroes = __builtin_ctzll(asInt); + auto impliedAllocationSize = 1 << trailingZeroes; + auto roundSizeToAllocationSizeMask = impliedAllocationSize - 1; + auto size = s.size() + 1; + if constexpr(IsZooStr::value) { + size += zoo::Log256Celing(size); + } + // example, allocation size is 64, the mask is then 0b1.1111 (5 bits) + // and size is 17: that's 0b1.0001, the bytes 18 to 63 are not used, + // so, the bytes that this code takes are 63 + return (size | roundSizeToAllocationSizeMask) + sizeof(STR); +} + +using ZStr = zoo::Str; + +namespace zoo { + +template +bool operator<( + const Str &left, + const Str &right +) { + auto lS = left.size(), rS = right.size(); + auto minLength = lS < rS ? lS : rS; + auto preRV = memcmp(left.c_str(), right.c_str(), minLength); + return preRV < 0 || (0 == preRV && lS < rS); +} + +} + +#include +// by ChatGPT +std::string toEngineeringString(double value, int precision) { + if (value == 0.0) { + return "0.0"; + } + + int exponent = static_cast(std::floor(std::log10(std::abs(value)))); + int engineeringExponent = exponent - (exponent % 3); // Align to multiple of 3 + double scaledValue = value / std::pow(10, engineeringExponent); + + // Use a stringstream for formatting + std::ostringstream oss; + oss << std::fixed << std::setprecision(precision); + oss << scaledValue << "e" << engineeringExponent; + + return oss.str(); +} + +TEST_CASE("Efficiency counters") { + auto [strs, _] = loadTextCorpus( + "/tmp/deleteme/TheTurnOfTheScrew.txt.lowercase.txt" + ); + constexpr char LongString[] = + "A very long string, contents don't matter, just size"; + SECTION("minimum tests") { + REQUIRE(sizeof(std::string) == minimumUsedBytes(std::string("Hola"))); + REQUIRE(sizeof(std::string) < minimumUsedBytes(std::string(LongString))); + } + strs.push_back(LongString); + + auto process = [](auto &strings) { + auto + allocationCount = 0, + significantBytes = 0, + totalBytesCommitted = 0; + auto stringCount = strings.size() + 1; + for(auto &s: strings) { + if(onHeap(s)) { ++allocationCount; } + significantBytes += s.size(); + totalBytesCommitted += minimumUsedBytes(s); + } + auto average = [stringCount](double v) { + return toEngineeringString(v / stringCount, 3); + }; + WARN( +sizeof(typename std::remove_reference_t::value_type) << ' ' << typeid(decltype(strings)).name() << +"\nCount: " << stringCount << +"\nAllocations: " << allocationCount << +"\nSignificant Bytes: " << significantBytes << +"\nTotalBytes: " << totalBytesCommitted << +"\nEfficiency: " << + toEngineeringString(significantBytes/double(totalBytesCommitted), 3) << +"\nAverages (allocations, size)" << + average(allocationCount) << ' ' << average(significantBytes) << +"\n" + ); + }; + SECTION("std") { + process(strs); + } + + #define QUOTE(a) #a + #define STRINGIFY(a) QUOTE(a) + #define STORAGE_PROTOTYPE_X_LIST \ + X(void *)\ + X(void *[2])\ + X(void *[3])\ + X(void *[4]) + #define X(prototype) \ + SECTION("zoo::Str<" STRINGIFY(prototype) ">") { \ + std::vector> zoos; \ + for(auto &s: strs) { zoos.emplace_back(s.c_str(), s.size() + 1); } \ + process(zoos); \ + } + STORAGE_PROTOTYPE_X_LIST +} + +TEST_CASE("Str benchmarks") { + auto [strings, integers] = loadTextCorpus( + "/tmp/deleteme/TheTurnOfTheScrew.txt.lowercase.txt" + ); + std::vector zss; + for(auto &source: strings) { + zss.push_back({source.data(), source.size() + 1}); + } + REQUIRE(strings.size() == integers.size()); + REQUIRE(zss.size() == integers.size()); + auto buildHistogram = + [](auto &histogram, const auto &events) { + for(auto &event: events) { + ++histogram[event]; + } + }; + auto &strs = strings; + auto &ints = integers; + + std::map zooH; + std::map intH; + std::map stdH; + + auto hSize = [&](auto &h, auto &series) { + h.clear(); + buildHistogram(h, series); + return h.size(); + }; + auto iS = hSize(intH, ints); + REQUIRE(hSize(zooH, zss) == iS); + + BENCHMARK("zoo::Str") { + zooH.clear(); + buildHistogram(zooH, zss); + return zooH.size(); + }; + BENCHMARK("Baseline") { + intH.clear(); + buildHistogram(intH, ints); + return intH.size(); + }; + BENCHMARK("std::string") { + stdH.clear(); + buildHistogram(stdH, strs); + return stdH.size(); + }; + BENCHMARK("zoo::Str 2") { + zooH.clear(); + buildHistogram(zooH, zss); + return zooH.size(); + }; + std::map, int> zooTwoPointersH; + std::vector> twoPSeries; + for(auto &source: strings) { + twoPSeries.push_back({ source.data(), source.size() + 1 }); + } + BENCHMARK("zoo::Str") { + zooTwoPointersH.clear(); + buildHistogram(zooTwoPointersH, twoPSeries); + return zooTwoPointersH.size(); + }; + + std::map, int> zooTwoPointersH8; + std::vector> twoPSeries8; + for(auto &source: strings) { + twoPSeries8.push_back({ source.data(), source.size() + 1 }); + } + BENCHMARK("zoo::Str") { + zooTwoPointersH8.clear(); + buildHistogram(zooTwoPointersH8, twoPSeries8); + return zooTwoPointersH8.size(); + }; + +} diff --git a/benchmark/string.catch2.cpp b/benchmark/string.catch2.cpp new file mode 100644 index 0000000..d06d4f5 --- /dev/null +++ b/benchmark/string.catch2.cpp @@ -0,0 +1,82 @@ +#include "zoo/Str.h" + +#include + +using S = zoo::Str; + +static_assert(std::is_nothrow_default_constructible_v); + +char BufferAsBigAsStr[sizeof(S)]; +static_assert(noexcept(S{BufferAsBigAsStr})); + +char BufferLargerThanStr[sizeof(S) * 2]; +static_assert(not noexcept(S{BufferLargerThanStr})); +static_assert(std::is_nothrow_move_constructible_v); + +constexpr char chars257[] = "\ +0123456789ABCDEF0123456789abcdef0123456789ABCDEF0123456789abcdef\ +we can put anything here, since we can see it is 64-bytes wide--\ +continuing, spaces are also good -\ +this is the last line !!\ +"; +static_assert(257 == sizeof(chars257)); + +template +auto stringChecks(const char *givenString, bool onHeap) { + auto facilitateComparisons = std::string(givenString); + StringType s(givenString, facilitateComparisons.length() + 1); + auto isOnHeap = s.onHeap(); + CHECK(onHeap == isOnHeap); + + auto c_str = s.c_str(); + CHECK(c_str == facilitateComparisons); + CHECK(strlen(c_str) == facilitateComparisons.size()); + auto zooSize = s.size(); + CHECK(zooSize == facilitateComparisons.size()); +}; + +TEST_CASE("String", "[str][api]") { + SECTION("Potential regression") { + char buff[8] = { "****#!-" }; + char Hi[] = { "Hello" }; + REQUIRE('\0' == buff[7]); + auto inPlaced = new(buff) S(Hi, sizeof(Hi)); + REQUIRE('\0' == buff[5]); + REQUIRE('-' == buff[6]); + REQUIRE('\x2' == buff[7]); + } + SECTION("Empty/defaulted") { + S empty; + CHECK(not empty.onHeap()); + CHECK(0 == empty.size()); + CHECK('\0' == *empty.c_str()); + } + constexpr char Exactly7[] = { "1234567" }; + char Exactly15[] = "123456789ABCDEF"; + constexpr char Quixote[] = "\ +En un lugar de la Mancha que no quiero recordar\ +"; + auto *checks = stringChecks; + SECTION("Local") { + SECTION("Not boundary") { + checks("Hello", false); + } + SECTION("Boundary of all chars needed") { + static_assert(8 == sizeof(S)); + checks(Exactly7, false); + } + } + SECTION("Heap") { + checks(Exactly15, true); + checks(Quixote, true); + SECTION("Case in which 1 < the bytes for size") { + checks(chars257, true); + } + } + SECTION("Larger than void *") { + using S2 = zoo::Str; + stringChecks(Exactly7, false); + stringChecks(Exactly15, false); + stringChecks(Quixote, true); + } +} diff --git a/inc/zoo/AnyContainer.h b/inc/zoo/AnyContainer.h index 55e80cf..f7943ef 100644 --- a/inc/zoo/AnyContainer.h +++ b/inc/zoo/AnyContainer.h @@ -298,7 +298,7 @@ struct MSVC_EMPTY_BASES AnyContainerBase: {} template - void emplaced(ValueType *ptr) noexcept { SuperContainer::template emplaced(ptr); } + void emplaced(ValueType *ptr) noexcept { SuperContainer::emplaced(ptr); } AnyContainerBase ©_assign(const AnyContainerBase &model) { SuperContainer::copy_assign(model);