You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
The C++20 Concept Trap of std::inserter and std::array#2
When writing a generic function template that populates an output container—using std::inserter for dynamic containers (std::vector, std::deque, std::list) and falling back to overwriting elements via std::ranges::begin for fixed-size containers (std::array<T, N>)—it is natural to reach for a C++20 concept and if constexpr.
However, combining C++98 iterator adaptors (std::inserter) with C++20 requires-expressions creates a subtle two-layer trap.
Attempt 1: Checking if std::inserter can be constructed
The first instinct is to check whether std::inserter(x, std::ranges::end(x)) is a valid expression:
Surprisingly, naive_is_inserterable<std::array<int, 4>> evaluates to true!
Because std::inserter only constructs a std::insert_iterator<T> proxy object (storing a pointer to the container and an iterator), it never checks whether .insert() exists during construction. When std::array enters the if constexpr branch for std::inserter, it triggers a hard compilation error ('insert': is not a member of 'std::array<...>') inside the algorithm body as the following pseudocode:
template<std::ranges::input_range Range>
voidfoo(Range& container)
{
ifconstexpr (naive_is_inserterable<Range>)
{
// Passes std::inserter(container, ...) to an algorithm (which invokes container.insert()).// If Range is std::array<int, 4>, this branch will still be instantiated and fail to compile!
}
else
{
// Fallback for fixed-size containers
}
}
Attempt 2: Simulating element assignment (*std::inserter(...) = v)
Since the actual .insert() call lives inside std::insert_iterator::operator=, the next logical step is to simulate an assignment inside the requires-expression:
Counter-intuitively, assign_is_inserterable<std::array<int, 4>>still evaluates to true! Why?
Unevaluated Operands ([expr.prim.req]): Expressions inside a requires block are unevaluated. The compiler only checks whether the member function declaration of std::insert_iterator<T>::operator= matches the argument types in the immediate context—it does not instantiate the body of operator=.
Non-Deduced Return Type & Unconstrained Signature: Because std::insert_iterator::operator=(const typename Container::value_type&) has an explicit, non-auto return type (insert_iterator&) and lacks a C++20 requires clause of its own, the compiler never inspects its function body (container->insert(iter, value)) during concept satisfaction checks. Since std::array provides both iterator and value_type, the declaration signature matches cleanly.
The Fix: Verifying x.insert(...) in the Immediate Context
To force the compiler to verify insertion capability during concept evaluation, we must query x.insert(...) directly on the container alongside std::inserter:
With this direct check, is_inserterable<std::array<int, 4>> properly evaluates to false. (Context: We encountered this while unifying multi-level container transformations in TinyDIP::recursive_transform so that mixed nested structures like std::vector<std::array<T, N>> can be transformed in a single template function.)
Minimal Reproducible Example
#include<algorithm>
#include<array>
#include<cassert>
#include<concepts>
#include<cstdlib>
#include<iostream>
#include<iterator>
#include<ranges>
#include<vector>namespaceTinyDIP
{
// Attempt 1: Only checks if std::inserter can be constructedtemplate<typename T>
concept naive_is_inserterable = requires(T& x)
{
std::inserter(x, std::ranges::end(x));
};
// Attempt 2: Simulates assignment (*std::inserter(...) = v)// Still evaluates to true for std::array!template<typename T>
concept assign_is_inserterable =
std::ranges::range<T> andrequires(T& x, std::ranges::range_value_t<T> v)
{
*std::inserter(x, std::ranges::end(x)) = v;
};
// The Fix: Verify x.insert(...) directly in the immediate contexttemplate<typename T>
concept is_inserterable =
std::ranges::range<T> andrequires(T& x, std::ranges::range_value_t<T> v)
{
x.insert(std::ranges::end(x), v);
std::inserter(x, std::ranges::end(x));
};
// Minimal generic function demonstrating the if constexpr dispatchtemplate<typename OutputContainer, std::ranges::input_range InputRange>
constexpr OutputContainer populate_doubled(const InputRange& input)
{
OutputContainer output{};
auto double_op = [](constauto val)
{
return val * 2;
};
ifconstexpr (is_inserterable<OutputContainer>)
{
std::ranges::transform(
input,
std::inserter(output, std::ranges::end(output)),
double_op
);
}
else
{
std::ranges::transform(
input,
std::ranges::begin(output),
double_op
);
}
return output;
}
}
intmain()
{
using FixedArray = std::array<int, 4>;
// 1. Compile-time verification of the two traps vs. the fixstatic_assert(TinyDIP::naive_is_inserterable<FixedArray> == true);
static_assert(TinyDIP::assign_is_inserterable<FixedArray> == true);
static_assert(TinyDIP::is_inserterable<FixedArray> == false);
std::cout << "Concept evaluation for std::array<int, 4>:\n"
<< " Attempt 1 (naive_is_inserterable) : "
<< std::boolalpha << TinyDIP::naive_is_inserterable<FixedArray> << '\n'
<< " Attempt 2 (assign_is_inserterable) : "
<< std::boolalpha << TinyDIP::assign_is_inserterable<FixedArray> << '\n'
<< " The Fix (is_inserterable) : "
<< std::boolalpha << TinyDIP::is_inserterable<FixedArray> << "\n\n";
// 2. Test unified dispatch on both std::array and std::vectorconstexpr FixedArray source{ 1, 2, 3, 4 };
constexprauto arr_result = TinyDIP::populate_doubled<FixedArray>(source);
constauto vec_result = TinyDIP::populate_doubled<std::vector<int>>(source);
assert(arr_result[3] == 8);
assert(vec_result[3] == 8);
std::cout << "Populated std::array<int, 4> : [ ";
for (constint v : arr_result)
{
std::cout << v << '';
}
std::cout << "]\n";
std::cout << "Populated std::vector<int> : [ ";
for (constint v : vec_result)
{
std::cout << v << '';
}
std::cout << "]\n";
returnEXIT_SUCCESS;
}
Should C++ standard library implementations (or a future defect report) constrain std::insert_iterator::operator= and std::back_insert_iterator::operator= on the underlying container operations (insert, push_back), or would that break valid legacy code?
How do you prefer to structure insertion traits when writing generic functions that support both dynamic containers and std::array?
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
When writing a generic function template that populates an output container—using
std::inserterfor dynamic containers (std::vector,std::deque,std::list) and falling back to overwriting elements viastd::ranges::beginfor fixed-size containers (std::array<T, N>)—it is natural to reach for a C++20conceptandif constexpr.However, combining C++98 iterator adaptors (
std::inserter) with C++20requires-expressions creates a subtle two-layer trap.Attempt 1: Checking if
std::insertercan be constructedThe first instinct is to check whether
std::inserter(x, std::ranges::end(x))is a valid expression:Surprisingly,
naive_is_inserterable<std::array<int, 4>>evaluates totrue!Because
std::inserteronly constructs astd::insert_iterator<T>proxy object (storing a pointer to the container and an iterator), it never checks whether.insert()exists during construction. Whenstd::arrayenters theif constexprbranch forstd::inserter, it triggers a hard compilation error ('insert': is not a member of 'std::array<...>') inside the algorithm body as the following pseudocode:Attempt 2: Simulating element assignment (
*std::inserter(...) = v)Since the actual
.insert()call lives insidestd::insert_iterator::operator=, the next logical step is to simulate an assignment inside therequires-expression:Counter-intuitively,
assign_is_inserterable<std::array<int, 4>>still evaluates totrue! Why?[expr.prim.req]): Expressions inside arequiresblock are unevaluated. The compiler only checks whether the member function declaration ofstd::insert_iterator<T>::operator=matches the argument types in the immediate context—it does not instantiate the body ofoperator=.std::insert_iterator::operator=(const typename Container::value_type&)has an explicit, non-autoreturn type (insert_iterator&) and lacks a C++20requiresclause of its own, the compiler never inspects its function body (container->insert(iter, value)) during concept satisfaction checks. Sincestd::arrayprovides bothiteratorandvalue_type, the declaration signature matches cleanly.The Fix: Verifying
x.insert(...)in the Immediate ContextTo force the compiler to verify insertion capability during concept evaluation, we must query
x.insert(...)directly on the container alongsidestd::inserter:With this direct check,
is_inserterable<std::array<int, 4>>properly evaluates tofalse.(Context: We encountered this while unifying multi-level container transformations in
TinyDIP::recursive_transformso that mixed nested structures likestd::vector<std::array<T, N>>can be transformed in a single template function.)Minimal Reproducible Example
Godbolt Link
Discussion Questions
Should C++ standard library implementations (or a future defect report) constrain
std::insert_iterator::operator=andstd::back_insert_iterator::operator=on the underlying container operations (insert,push_back), or would that break valid legacy code?How do you prefer to structure insertion traits when writing generic functions that support both dynamic containers and
std::array?All reactions