From 5caf9f49cbfe5f469721ed15dc988628abd7b307 Mon Sep 17 00:00:00 2001 From: Sebastian Wieczorek Date: Mon, 10 Feb 2025 14:06:44 +0100 Subject: [PATCH] c++17 concepts with CPP_template for engine/idTable/ files (#1746) Backport C++20 concepts in the `src/engine/idTable` directory back to C++17 using macros from `range-v3` --- .../idTable/CompressedExternalIdTable.h | 23 ++- src/engine/idTable/IdTable.h | 191 +++++++++++------- src/engine/idTable/IdTableRow.h | 24 ++- src/util/StringUtils.h | 4 +- test/IdTableTest.cpp | 2 + 5 files changed, 157 insertions(+), 87 deletions(-) diff --git a/src/engine/idTable/CompressedExternalIdTable.h b/src/engine/idTable/CompressedExternalIdTable.h index 970777c52d..3197c9e1e3 100644 --- a/src/engine/idTable/CompressedExternalIdTable.h +++ b/src/engine/idTable/CompressedExternalIdTable.h @@ -22,6 +22,14 @@ namespace ad_utility { +namespace compressedExternalIdTable::detail { +template +CPP_requires(HasPushBackRequires, requires(B& b, const R& r)(b.push_back(r))); + +template +CPP_concept HasPushBack = CPP_requires_ref(HasPushBackRequires, B, R); +} // namespace compressedExternalIdTable::detail + using namespace ad_utility::memory_literals; // The default size for compressed blocks in the following classes. @@ -338,8 +346,9 @@ class CompressedExternalIdTableBase { } // Add a single row to the input. The type of `row` needs to be something that // can be `push_back`ed to a `IdTable`. - void push(const auto& row) requires requires { currentBlock_.push_back(row); } - { + CPP_template(typename R)( + requires compressedExternalIdTable::detail::HasPushBack< + decltype(currentBlock_), R>) void push(const R& row) { ++numElementsPushed_; currentBlock_.push_back(row); if (currentBlock_.size() >= blocksize_) { @@ -456,11 +465,11 @@ class CompressedExternalIdTable // When we have a static number of columns, then the `numCols` argument to the // constructor is redundant. - explicit CompressedExternalIdTable( + CPP_member explicit CPP_ctor(CompressedExternalIdTable)( std::string filename, ad_utility::MemorySize memory, ad_utility::AllocatorWithLimit allocator, - MemorySize blocksizeCompression = DEFAULT_BLOCKSIZE_EXTERNAL_ID_TABLE) - requires(NumStaticCols > 0) + MemorySize blocksizeCompression = DEFAULT_BLOCKSIZE_EXTERNAL_ID_TABLE)( + requires(NumStaticCols > 0)) : CompressedExternalIdTable(std::move(filename), NumStaticCols, memory, std::move(allocator), blocksizeCompression) {} @@ -583,11 +592,11 @@ class CompressedExternalIdTableSorter // When we have a static number of columns, then the `numCols` argument to the // constructor is redundant. - CompressedExternalIdTableSorter( + CPP_member CPP_ctor(CompressedExternalIdTableSorter)( std::string filename, ad_utility::MemorySize memory, ad_utility::AllocatorWithLimit allocator, MemorySize blocksizeCompression = DEFAULT_BLOCKSIZE_EXTERNAL_ID_TABLE, - Comparator comp = {}) requires(NumStaticCols > 0) + Comparator comp = {})(requires(NumStaticCols > 0)) : CompressedExternalIdTableSorter(std::move(filename), NumStaticCols, memory, std::move(allocator), blocksizeCompression, comp) {} diff --git a/src/engine/idTable/IdTable.h b/src/engine/idTable/IdTable.h index 5a046b80f6..c06c16d7f2 100644 --- a/src/engine/idTable/IdTable.h +++ b/src/engine/idTable/IdTable.h @@ -182,12 +182,14 @@ class IdTable { // Then the argument `numColumns` and `NumColumns` (the static and the // dynamic number of columns) must be equal, else a runtime check fails. // Note: this also allows to create an empty view. - explicit IdTable(size_t numColumns) - requires(columnsAreAllocatable && - std::is_default_constructible_v) + CPP_template(typename = void)( + requires columnsAreAllocatable CPP_and std::is_default_constructible_v< + Allocator>) explicit IdTable(size_t numColumns) : IdTable(numColumns, Allocator{}) {} - explicit IdTable(size_t numColumns, Allocator allocator) - requires columnsAreAllocatable + + CPP_template(typename = void)( + requires(columnsAreAllocatable)) explicit IdTable(size_t numColumns, + Allocator allocator) : numColumns_{numColumns}, allocator_{std::move(allocator)} { if constexpr (!isDynamic) { AD_CONTRACT_CHECK(NumColumns == numColumns); @@ -204,8 +206,9 @@ class IdTable { // fails. Additional columns (if `columns.size() > numColumns`) are deleted. // This behavior is useful for unit tests Where we can just generically pass // in more columns than are needed in any test. - CPP_template(typename ColT)(requires ql::ranges::forward_range) - IdTable(size_t numColumns, ColT columns) requires(!isView) + CPP_template(typename ColT)( + requires ql::ranges::forward_range CPP_and CPP_NOT(isView)) + IdTable(size_t numColumns, ColT columns) : data_{std::make_move_iterator(columns.begin()), std::make_move_iterator(columns.end())}, numColumns_{numColumns} { @@ -224,26 +227,38 @@ class IdTable { // already set up with the correct number of columns and can be used directly. // If `NumColumns == 0` then the number of columns has to be specified via // `setNumColumns()`. - IdTable() requires(!isView && columnsAreAllocatable && - std::is_default_constructible_v) + CPP_template(typename = void)( + requires CPP_NOT(isView) CPP_and columnsAreAllocatable CPP_and + std::is_default_constructible_v) IdTable() : IdTable{NumColumns, Allocator{}} {}; - explicit IdTable(Allocator allocator) - requires(!isView && columnsAreAllocatable) + + CPP_template(typename = void)( + requires CPP_NOT(isView) + CPP_and columnsAreAllocatable) explicit IdTable(Allocator allocator) : IdTable{NumColumns, std::move(allocator)} {}; // `IdTables` are expensive to copy, so we disable accidental copies as they // are most likely bugs. To explicitly copy an `IdTable`, the `clone()` member // function (see below) can be used. + // Note: We currently only disable the copy operations in C++20 mode. + // TODO implement a facility (probably via inheritance) where we can + // also implement the deleted copy operations for C++17 +#ifndef QLEVER_CPP_17 IdTable(const IdTable&) requires(!isView) = delete; IdTable& operator=(const IdTable&) requires(!isView) = delete; - // Views are copyable, as they are cheap to copy. + // Views are copyable, as they are cheap to copy. IdTable(const IdTable&) requires isView = default; IdTable& operator=(const IdTable&) requires isView = default; +#else + IdTable(const IdTable&) = default; + IdTable& operator=(const IdTable&) = default; +#endif + // `IdTable`s are movable - IdTable(IdTable&& other) noexcept requires(!isView) = default; - IdTable& operator=(IdTable&& other) noexcept requires(!isView) = default; + IdTable(IdTable&& other) noexcept = default; + IdTable& operator=(IdTable&& other) noexcept = default; private: // Make the other instantiations of `IdTable` friends to allow for conversion @@ -273,7 +288,8 @@ class IdTable { public: // For an empty and dynamic (`NumColumns == 0`) `IdTable`, specify the // number of columns. - void setNumColumns(size_t numColumns) requires columnsAreAllocatable { + CPP_template(typename = void)( + requires(columnsAreAllocatable)) void setNumColumns(size_t numColumns) { AD_CONTRACT_CHECK(empty()); AD_CONTRACT_CHECK(isDynamic || numColumns == NumColumns); numColumns_ = numColumns; @@ -281,7 +297,8 @@ class IdTable { } // Add a new empty column to the table. - void addEmptyColumn() requires columnsAreAllocatable && isDynamic { + CPP_template(typename = void)( + requires columnsAreAllocatable CPP_and isDynamic) void addEmptyColumn() { data().emplace_back(size(), allocator_); ++numColumns_; } @@ -314,7 +331,8 @@ class IdTable { // for performance reason whenever possible. // TODO Use the multidimensional subscript operator. // TODO Use explicit object parameters ("deducing this"). - T& operator()(size_t row, size_t column) requires(!isView) { + CPP_template(typename = void)(requires(!isView)) T& operator()( + size_t row, size_t column) { AD_EXPENSIVE_CHECK(column < data().size(), [&]() { return absl::StrCat(row, " , ", column, ", ", data().size(), " ", numColumns(), ", ", numStaticColumns); @@ -329,15 +347,19 @@ class IdTable { // Get safe access to a single element specified by the row and the column. // Throw if the row or the column is out of bounds. See the note for // `operator()` above. - T& at(size_t row, size_t column) requires(!isView) { + CPP_template(typename = void)(requires(!isView)) T& at(size_t row, + size_t column) + requires(!isView) { return data().at(column).at(row); } // TODO Remove overload for `isView` and drop requires clause. - const T& at(size_t row, size_t column) const requires(!isView) { + CPP_template(typename = void)(requires(!isView)) const T& at( + size_t row, size_t column) const { return data().at(column).at(row); } // `std::span::at` is a C++26 feature, so we have to implement it ourselves. - const T& at(size_t row, size_t column) const requires(isView) { + CPP_template(typename = void)(requires(isView)) const T& at( + size_t row, size_t column) const requires(isView) { const auto& col = data().at(column); AD_CONTRACT_CHECK(row < col.size()); return col[row]; @@ -346,7 +368,8 @@ class IdTable { // Get a reference to the `i`-th row. The returned proxy objects can be // implicitly and trivially converted to `row_reference`. For the design // rationale behind those proxy types see above for their definition. - row_reference_restricted operator[](size_t index) requires(!isView) { + CPP_template(typename = void)(requires(!isView)) row_reference_restricted + operator[](size_t index) { return *(begin() + index); } const_row_reference_restricted operator[](size_t index) const { @@ -355,7 +378,8 @@ class IdTable { // Same as operator[], but throw an exception if the `row` is out of bounds. // This is similar to the behavior of `std::vector::at`. - row_reference_restricted at(size_t row) requires(!isView) { + CPP_template(typename = void)(requires(!isView)) row_reference_restricted + at(size_t row) { AD_CONTRACT_CHECK(row < numRows()); return operator[](row); } @@ -368,9 +392,15 @@ class IdTable { // `std::vector` and other containers. // TODO Remove the duplicates via explicit object parameters // ("deducing this"). - row_reference_restricted front() requires(!isView) { return (*this)[0]; } + CPP_template(typename = void)(requires(!isView)) row_reference_restricted + front() { + return (*this)[0]; + } + const_row_reference_restricted front() const { return (*this)[0]; } - row_reference_restricted back() requires(!isView) { + + CPP_template(typename = void)(requires(!isView)) row_reference_restricted + back() { return (*this)[numRows() - 1]; } const_row_reference_restricted back() const { return (*this)[numRows() - 1]; } @@ -383,7 +413,7 @@ class IdTable { // // Note: The semantics of this function is similar to `std::vector::resize`. // To set the capacity, use the `reserve` function. - void resize(size_t numRows) requires(!isView) { + CPP_template(typename = void)(requires(!isView)) void resize(size_t numRows) { ql::ranges::for_each(data(), [numRows](auto& column) { column.resize(numRows); }); numRows_ = numRows; @@ -395,7 +425,8 @@ class IdTable { // iterators are invalidated, but you obtain the guarantee, that the insertion // of the next `numRows - size()` elements (via `insert` or `push_back`) can // be done in O(1) time without dynamic allocations. - void reserve(size_t numRows) requires(!isView) { + CPP_template(typename = void)(requires(!isView)) void reserve( + size_t numRows) { ql::ranges::for_each(data(), [numRows](auto& column) { column.reserve(numRows); }); } @@ -403,7 +434,7 @@ class IdTable { // Delete all the elements, but keep the allocated memory (`capacityRows_` // stays the same). Runs in O(1) time. To also free the allocated memory, call // `shrinkToFit()` after calling `clear()` . - void clear() requires(!isView) { + CPP_template(typename = void)(requires(!isView)) void clear() { numRows_ = 0; ql::ranges::for_each(data(), [](auto& column) { column.clear(); }); } @@ -411,7 +442,7 @@ class IdTable { // Adjust the capacity to exactly match the size. This optimizes the memory // consumption of this table. This operation runs in O(size()), allocates // memory, and invalidates all iterators. - void shrinkToFit() requires(!isView) { + CPP_template(typename = void)(requires(!isView)) void shrinkToFit() { ql::ranges::for_each(data(), [](auto& column) { column.shrink_to_fit(); }); } @@ -422,7 +453,7 @@ class IdTable { // `O(n)`. The underlying data model is a dynamic array like `std::vector`. // Insert a new uninitialized row at the end. - void emplace_back() requires(!isView) { + CPP_template(typename = void)(requires(!isView)) void emplace_back() { ql::ranges::for_each(data(), [](auto& column) { column.emplace_back(); }); ++numRows_; } @@ -432,10 +463,9 @@ class IdTable { // fail (in Debug mode). The `newRow` can be any random access range that // stores the right type and has the right size. CPP_template(typename RowLike)( - requires ql::ranges::random_access_range CPP_and - std::same_as, - T>) void push_back(const RowLike& newRow) - requires(!isView) { + requires CPP_NOT(isView) CPP_and ql::ranges::random_access_range + CPP_and std::same_as, + T>) void push_back(const RowLike& newRow) { AD_EXPENSIVE_CHECK(newRow.size() == numColumns()); ++numRows_; ql::ranges::for_each(ad_utility::integerRange(numColumns()), @@ -444,7 +474,8 @@ class IdTable { }); } - void push_back(const std::initializer_list& newRow) requires(!isView) { + CPP_template(typename = void)(requires(!isView)) void push_back( + const std::initializer_list& newRow) { push_back(ql::ranges::ref_view{newRow}); } @@ -459,8 +490,8 @@ class IdTable { // `true`), then the copy constructor will also create a (const and // non-owning) view, but `clone` will create a mutable deep copy of the data // that the view points to - IdTable clone() const - requires isCloneable { + CPP_template(typename = void)(requires(isCloneable)) + IdTable clone() const { Storage storage; for (const auto& column : getColumns()) { storage.emplace_back(column.begin(), column.end(), getAllocator()); @@ -474,15 +505,23 @@ class IdTable { // `auto newTable = AD_FWD(oldTable).moveOrClone()` which is equivalent to the // pattern `auto newX = AD_FWD(oldX)` where the type is copy-constructible // (which `IdTable` is not.). - auto moveOrClone() const& requires isCloneable { return clone(); } - IdTable&& moveOrClone() && requires isCloneable { return std::move(*this); } + CPP_member auto moveOrClone() const& -> CPP_ret(IdTable)( + requires isCloneable) { + return clone(); + } + + CPP_member auto moveOrClone() && -> CPP_ret(IdTable&&)(requires isCloneable) { + return std::move(*this); + } // Overload of `clone` for `Storage` types that are not copy constructible. // It requires a preconstructed but empty argument of type `Storage` that // is then resized and filled with the appropriate contents. - IdTable clone( - std::vector newColumns, Allocator allocator = {}) const - requires(!std::is_copy_constructible_v) { + CPP_template(typename = void)( + requires(!std::is_copy_constructible_v)) + IdTable clone( + std::vector newColumns, + Allocator allocator = {}) const { AD_CONTRACT_CHECK(newColumns.size() >= numColumns()); Data newStorage(std::make_move_iterator(newColumns.begin()), std::make_move_iterator(newColumns.begin() + numColumns())); @@ -505,10 +544,10 @@ class IdTable { // in fact moves a dynamic table to a new dynamic table. This makes // generic code that is templated on the number of columns easier to // write. - template - requires((isDynamic || NewNumColumns == NumColumns || NewNumColumns == 0) && - !isView) - IdTable toStatic() && { + CPP_template(int NewNumColumns)( + requires(isDynamic || NewNumColumns == NumColumns || NewNumColumns == 0) + CPP_and CPP_NOT( + isView)) IdTable toStatic() && { AD_CONTRACT_CHECK(numColumns() == NewNumColumns || NewNumColumns == 0); auto result = IdTable{ std::move(data()), numColumns(), std::move(numRows_), @@ -519,28 +558,30 @@ class IdTable { // Move this `IdTable` into a dynamic `IdTable` with `NumColumns == 0`. This // function may only be called on rvalues, because the table will be moved // from. - IdTable toDynamic() && requires(!isView) { + CPP_template(typename = void)( + requires(!isView)) IdTable toDynamic() && { auto result = IdTable{std::move(data()), numColumns_, std::move(numRows_), std::move(allocator_)}; return result; } - // Given a dynamic (`NumColumns == 0`) IdTable, create a static (`NumColumns - // != 0`) view of an `IdTable` with `NumColumns == NewNumColumns`. The number - // of columns in the dynamic table must either be equal to `NewNumColumns`, or - // the dynamic table must be empty; otherwise a runtime check fails. The - // created view is `const` and only contains a pointer to the table from which - // it was created. Therefore, calling this function is cheap (O(1)), but the - // created view is only valid as long as the original table is valid and - // unchanged. + // Given a dynamic (`NumColumns == 0`) IdTable, create a static + // (`NumColumns + // != 0`) view of an `IdTable` with `NumColumns == NewNumColumns`. The + // number of columns in the dynamic table must either be equal to + // `NewNumColumns`, or the dynamic table must be empty; otherwise a + // runtime check fails. The created view is `const` and only contains a + // pointer to the table from which it was created. Therefore, calling this + // function is cheap (O(1)), but the created view is only valid as long as + // the original table is valid and unchanged. // // Note: This function can also be used with `NewNumColumns == 0`. Then it - // creates a dynamic view from a dynamic table. This makes generic code that - // is templated on the number of columns easier to write. - template - requires(isDynamic || NewNumColumns == 0) - IdTable asStaticView() const { + // creates a dynamic view from a dynamic table. This makes generic code + // that is templated on the number of columns easier to write. + CPP_template(size_t NewNumColumns)(requires(isDynamic || NewNumColumns == 0)) + IdTable asStaticView() + const { AD_CONTRACT_CHECK(numColumns() == NewNumColumns || NewNumColumns == 0); ViewSpans viewSpans(data().begin(), data().end()); @@ -551,8 +592,9 @@ class IdTable { // Obtain a dynamic and const view to this IdTable that contains a subset of // the columns that may be permuted. The subset of the columns is specified by // the argument `columnIndices`. - IdTable asColumnSubsetView( - std::span columnIndices) const requires isDynamic { + CPP_template(typename = void)(requires isDynamic) + IdTable asColumnSubsetView( + std::span columnIndices) const { AD_CONTRACT_CHECK(ql::ranges::all_of( columnIndices, [this](size_t idx) { return idx < numColumns(); })); ViewSpans viewSpans; @@ -604,7 +646,8 @@ class IdTable { } // Delete the column with the given column index. - void deleteColumn(ColumnIndex colIdx) requires isDynamic { + CPP_template(typename = void)(requires isDynamic) void deleteColumn( + ColumnIndex colIdx) { AD_CONTRACT_CHECK(colIdx < numColumns()); data().erase(data().begin() + colIdx); numColumns_--; @@ -647,8 +690,12 @@ class IdTable { // The usual overloads of `begin()` and `end()` for const and mutable // `IdTable`s. - iterator begin() requires(!isView) { return {this, 0}; } - iterator end() requires(!isView) { return {this, size()}; } + CPP_template(typename = void)(requires(!isView)) iterator begin() { + return {this, 0}; + } + CPP_template(typename = void)(requires(!isView)) iterator end() { + return {this, size()}; + } const_iterator begin() const { return {this, 0}; } const_iterator end() const { return {this, size()}; } @@ -662,7 +709,8 @@ class IdTable { // that `begin() <= beginIt <= endIt < end`, else the behavior is undefined. // The order of the elements before and after the erased regions remains the // same. This behavior is similar to `std::vector::erase`. - void erase(const iterator& beginIt, const iterator& endIt) requires(!isView) { + CPP_member auto erase(const iterator& beginIt, const iterator& endIt) + -> CPP_ret(void)(requires(!isView)) { AD_EXPENSIVE_CHECK(begin() <= beginIt && beginIt <= endIt && endIt <= end()); auto startIndex = beginIt - begin(); @@ -677,7 +725,10 @@ class IdTable { // Erase the single row that `it` points to by shifting all the elements // after `it` towards the beginning. Requires that `begin() <= it < end()`, // otherwise the behavior is undefined. - void erase(const iterator& it) requires(!isView) { erase(it, it + 1); } + CPP_member auto erase(const iterator& it) + -> CPP_ret(void)(requires(!isView)) { + erase(it, it + 1); + } // Add all entries from the `table` at the end of this IdTable. // If `beginIdx` and/or `endIdx` are specified, then only the subrange @@ -705,7 +756,8 @@ class IdTable { // Check whether two `IdTables` have the same content. Mostly used for unit // testing. - bool operator==(const IdTable& other) const requires(!isView) { + CPP_template(typename = void)(requires(!isView)) bool operator==( + const IdTable& other) const { if (numColumns() != other.numColumns()) { return (empty() && other.empty()); } @@ -728,7 +780,10 @@ class IdTable { } // Get the `i`-th column. It is stored contiguously in memory. - std::span getColumn(size_t i) requires(!isView) { return {data().at(i)}; } + CPP_template(typename = void)(requires(!isView)) std::span getColumn( + size_t i) { + return {data().at(i)}; + } std::span getColumn(size_t i) const { return {data().at(i)}; } // Return all the columns as a `std::vector` (if `isDynamic`) or as a diff --git a/src/engine/idTable/IdTableRow.h b/src/engine/idTable/IdTableRow.h index 23290d2305..cba93df96e 100644 --- a/src/engine/idTable/IdTableRow.h +++ b/src/engine/idTable/IdTableRow.h @@ -172,9 +172,11 @@ class RowReferenceImpl { protected: // The actual implementation of operator[]. - static T& operatorBracketImpl(auto& self, size_t i) - requires(!std::is_const_v> && - !isConst) { + CPP_template(typename SelfType)( + requires CPP_NOT(std::is_const_v>) + CPP_and CPP_NOT( + isConst)) static T& operatorBracketImpl(SelfType& self, + size_t i) { return (*self.table_)(self.row_, i); } static const T& operatorBracketImpl(const auto& self, size_t i) { @@ -232,7 +234,8 @@ class RowReferenceImpl { protected: // The implementation of swapping two `RowReference`s (passed either by // value or by reference). - static void swapImpl(auto&& a, auto&& b) requires(!isConst) { + CPP_template(typename AType, typename BType)( + requires(!isConst)) static void swapImpl(AType&& a, BType&& b) { for (size_t i = 0; i < a.numColumns(); ++i) { std::swap(operatorBracketImpl(a, i), operatorBracketImpl(b, i)); } @@ -246,15 +249,16 @@ class RowReferenceImpl { public: // Swap two `RowReference`s, but only if they are temporaries (rvalues). // This modifies the underlying table. - friend void swap(This&& a, This&& b) requires(!isConst) { + CPP_template(typename = void)(requires CPP_NOT(isConst)) friend void swap( + This&& a, This&& b) { return swapImpl(a, b); } // Equality comparison. Works between two `RowReference`s, but also between // a `RowReference` and a `Row` if the number of columns match. - template - bool operator==(const U& other) const - requires(numStaticColumns == U::numStaticColumns) { + CPP_template(typename U)(requires(numStaticColumns == + U::numStaticColumns)) bool + operator==(const U& other) const { if constexpr (numStaticColumns == 0) { if (numColumns() != other.numColumns()) { return false; @@ -279,8 +283,8 @@ class RowReferenceImpl { } // Convert from a static `RowReference` to a `std::array` (makes a copy). - explicit operator std::array() const - requires(numStaticColumns != 0) { + CPP_template(typename = void)(requires(numStaticColumns != 0)) explicit + operator std::array() const { std::array result; ql::ranges::copy(*this, result.begin()); return result; diff --git a/src/util/StringUtils.h b/src/util/StringUtils.h index f557682f0f..315d9daa8e 100644 --- a/src/util/StringUtils.h +++ b/src/util/StringUtils.h @@ -187,7 +187,7 @@ constexpr bool constantTimeEquals(std::string_view view1, } // _________________________________________________________________________ -CPP_template(typename Range)( +CPP_template_def(typename Range)( requires ql::ranges::input_range CPP_and ad_utility::Streamable>>) void lazyStrJoin(std::ostream* stream, Range&& r, @@ -213,7 +213,7 @@ CPP_template(typename Range)( } // _________________________________________________________________________ -CPP_template(typename Range)( +CPP_template_def(typename Range)( requires ql::ranges::input_range CPP_and ad_utility::Streamable< std::iter_reference_t>>) std::string lazyStrJoin(Range&& r, std::string_view separator) { diff --git a/test/IdTableTest.cpp b/test/IdTableTest.cpp index 441048a54e..438a7fdad8 100644 --- a/test/IdTableTest.cpp +++ b/test/IdTableTest.cpp @@ -1101,6 +1101,8 @@ TEST(IdTable, staticAsserts) { static_assert(std::is_trivially_copyable_v::const_iterator>); static_assert(ql::ranges::random_access_range); static_assert(ql::ranges::random_access_range>); + + static_assert(std::is_copy_constructible_v>); } TEST(IdTable, constructorsAreSfinaeFriendly) {