How to Calculate std::string_view Lengths at Compile Time for std::array Concatenation in C++
When writing high-performance compile-time code in modern C++ (C++20 and beyond), concatenating strings into a fixed-size std::array<char, N> is a frequent requirement. However, a common roadblock developers hit is trying to compute N inside a function using a regular parameter.
Even in a consteval function, you might run into compiler errors stating that a variable is not a constant expression. In this guide, we'll explain why this happens and explore the best modern solutions using Non-Type Template Parameters (NTTP) and variadic fold expressions.
The Problem: Why Function Parameters Aren't Constant Expressions
Consider the following attempt to compute the required buffer size from a std::array of std::string_view instances:
template<std::size_t SIZE>
consteval auto concat(const std::array<std::string_view, SIZE>& columns) noexcept {
// Compiler Error: columns is not a constant expression!
constexpr int size_texts = columns_size(columns);
std::array<char, size_texts> columns_concatenated{};
return columns_concatenated;
}Even though the function is declared consteval, the C++ standard dictates that function parameters can never be core constant expressions within the function body. Because columns is a parameter, the compiler cannot use its runtime representation to determine the return type size (std::array<char, size_texts>) at compile time.
To dictate the size of the returned array, the data used for the calculation must be available at the template level.
Solution 1: Use std::array as a Non-Type Template Parameter (C++20)
In C++20, literal structural types—such as std::array—can be passed directly as Non-Type Template Parameters (NTTP). This allows you to evaluate the contents of the array directly in a compile-time context.
#include <iostream>
#include <array>
#include <string_view>
#include <numeric>
// Calculate total size using an array as an NTTP
template <std::array Columns>
consteval auto concat() {
// Compute total length including null terminator
constexpr std::size_t total_length = []() {
std::size_t len = 0;
for (const auto& sv : Columns) {
len += sv.size();
}
return len + 1; // +1 for null terminator
}();
std::array<char, total_length> result{};
auto it = result.begin();
for (const auto& sv : Columns) {
for (char ch : sv) {
*it++ = ch;
}
}
*it = '\0';
return result;
}
int main() {
using namespace std::string_view_literals;
constexpr std::array columns = { "S.`OID`"sv, "S.`exchange_OID`"sv };
constexpr auto concatenated = concat<columns>();
std::cout << concatenated.data() << std::endl;
return 0;
}Solution 2: Variadic Templates with Fold Expressions
If you prefer a variadic approach rather than passing a fixed std::array, you can pass individual std::string_view instances or string wrappers as template arguments. Fold expressions make calculating the sum trivial:
#include <iostream>
#include <array>
#include <string_view>
template <std::string_view... Strs>
consteval auto concat_variadic() {
// Sum string sizes at compile time using a fold expression
constexpr std::size_t total_length = (Strs.size() + ...) + 1;
std::array<char, total_length> result{};
auto it = result.begin();
auto append = [&](std::string_view sv) {
for (char c : sv) *it++ = c;
};
(append(Strs), ...);
*it = '\0';
return result;
}
int main() {
using namespace std::string_view_literals;
static constexpr auto s1 = "S.`OID`"sv;
static constexpr auto s2 = "S.`exchange_OID`"sv;
constexpr auto merged = concat_variadic<s1, s2>();
std::cout << merged.data() << std::endl;
return 0;
}Solution 3: Unpacking an Array with std::index_sequence
If you already have a std::array and want to avoid passing it directly as an NTTP (for instance, to support older patterns or avoid large template instantiations), you can unpack the array elements into a variadic helper using std::index_sequence:
#include <iostream>
#include <array>
#include <string_view>
#include <utility>
template <const auto& Arr, std::size_t... Is>
consteval auto concat_indexed(std::index_sequence<Is...>) {
constexpr std::size_t total_size = (Arr[Is].size() + ...) + 1;
std::array<char, total_size> result{};
auto it = result.begin();
auto append = [&](std::string_view sv) {
for (char c : sv) *it++ = c;
};
(append(Arr[Is]), ...);
*it = '\0';
return result;
}
template <const auto& Arr>
consteval auto concat() {
return concat_indexed<Arr>(std::make_index_sequence<Arr.size()>{});
}
static constexpr std::array columns = {
std::string_view{"S.`OID`"},
std::string_view{"S.`exchange_OID`"}
};
int main() {
constexpr auto result = concat<columns>();
std::cout << result.data() << std::endl;
return 0;
}Summary
- The Cause: Function arguments cannot be used as constant expressions to define array dimensions, even within
constexprorconstevalfunctions. - The Fix: Shift the data from function parameters into template arguments.
- Modern Choice: In C++20, passing a structural type like
std::arraydirectly as a template parameter (template <std::array Columns>) is usually the cleanest and most readable solution.