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 constexpr or consteval functions.
  • The Fix: Shift the data from function parameters into template arguments.
  • Modern Choice: In C++20, passing a structural type like std::array directly as a template parameter (template <std::array Columns>) is usually the cleanest and most readable solution.