6

Would be possible to iterate over a constexpr C-array/std::array/vector and execute constexpr operations on each element and everything to be done at compile time?

https://godbolt.org/z/5a4v3Eerh

#include <string_view>
using namespace std::string_view_literals;

constexpr std::string_view arr[] = {"ab"sv, "xyz"sv,};

int main(){
    static_assert(arr[0] == "ab");  //OK
    static_assert(arr[1] == "xyz");

    #if 0 //would something like this be possible?
    for(const auto i: arr){
        static_assert(i == "xyz"); //???
    }
    #endif
    return 0;
}

I used static_assert() but there could be any constexpr function ...

Any suggestion would be appreciated! any C++ standard (preferably the latest), any compiler (preferably Microsoft VisualStudio 2022)

6
  • 1
    You assert on a constexpr function, e.g. static_assert(std::ranges::equal(arr, std::array{"ab", "xyz"})); Commented Sep 26, 2024 at 10:16
  • Please specify C++ standard. Based on use of static_assert looks like minimum C++17. Commented Sep 26, 2024 at 10:50
  • static_assert(std::count(begin(arr), end(arr), "ab"sv) == std::ssize(arr));? godbolt.org/z/zYhEevYTj Commented Sep 26, 2024 at 10:57
  • @MarekR the OP mentioned that the static_assert was just an example, and they want in general to perform any compile time operation on the array elements. Commented Sep 26, 2024 at 11:06
  • 1
    But static assert on each time do not carry any useful information. Compiler report still will not explain on which element assertion has failed. So why not just assert whole array? Commented Sep 26, 2024 at 11:11

6 Answers 6

9

This is a standard use case of std::index_sequence. C++20 templated lambdas make this easier.

    []<auto... Is>(std::index_sequence<Is...>){
        ([](){static_assert(arr[Is] == "xyz");}(), ...);
    }(std::make_index_sequence<std::size(arr)>());

The inner lambda is needed because static_assert is a statement and cannot be used as an expression.

Note that this solution is subject to the translation limits on the maximal number of template parameters and may also be subject to the limit of the maximal level of template instantiations. Therefore, it may fail to compile if the array is too large.

Sign up to request clarification or add additional context in comments.

1 Comment

Very nice, I tried and failed to make my solution a lambda. I was far from thinking about the wraping of static_assert.
4

Here is full example which prints useful diagnostics when assertions are falling:

namespace detail {
template <auto& Item, auto F>
constexpr bool checkItem()
{
    static_assert(F(Item));
    return F(Item);
}

template <auto& C, auto F, size_t... ints>
constexpr bool AllItemsAre(std::integer_sequence<size_t, ints...>)
{
    return (checkItem<C[ints], F>() && ...);
}
} // namespace detail

template <auto& C, auto F>
constexpr bool AllItemsAre()
{
    return detail::AllItemsAre<C, F>(std::make_index_sequence<std::size(C)> {});
}

Test code:

constexpr std::string_view arr[] = {
    "ab"sv,
    "xyz"sv,
#ifdef SHOW_FAILL
    "zaQzs"sv,
#endif
    "aad"sv,
};

constexpr bool isLower(std::string_view s)
{
    return std::all_of(s.begin(), s.end(), [](auto ch) { return ch >= 'a' && ch <= 'z'; });
}

static_assert(AllItemsAre<arr, &isLower>());

Live demo

In case assertion fails:

  • gcc prints:
<source>: In instantiation of 'constexpr bool detail::checkItem() [with auto& Item = arr[2]; auto F = isLower]':
<source>:17:34:   required from 'constexpr bool detail::AllItemsAre(std::integer_sequence<long unsigned int, ints ...>) [with auto& C = arr; auto F = isLower; long unsigned int ...ints = {0, 1, 2, 3}]'
   17 |     return (checkItem<C[ints], F>() && ...);
      |             ~~~~~~~~~~~~~~~~~~~~~^~
<source>:24:37:   required from here
   24 |     return detail::AllItemsAre<C, F>(std::make_index_sequence<std::size(C)> {});
      |            ~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:43:41:   in 'constexpr' expansion of 'AllItemsAre<arr, isLower>()'
<source>:43:32: error: static assertion failed
   43 | static_assert(AllItemsAre<arr, &isLower>());
      |                                ^~~~~~~~
<source>:43:32: note: 'isLower(arr[2])' evaluates to false
<source>:43:41: error: static assertion failed
   43 | static_assert(AllItemsAre<arr, &isLower>());
  • clang prints:
<source>:10:19: error: static assertion failed due to requirement '&isLower(<null expr>)'
   10 |     static_assert(F(Item));
      |                   ^~~~~~~
<source>:17:13: note: in instantiation of function template specialization 'detail::checkItem<arr[2], &isLower>' requested here
   17 |     return (checkItem<C[ints], F>() && ...);
      |             ^
<source>:24:20: note: in instantiation of function template specialization 'detail::AllItemsAre<arr, &isLower, 0UL, 1UL, 2UL, 3UL>' requested here
   24 |     return detail::AllItemsAre<C, F>(std::make_index_sequence<std::size(C)> {});
      |                    ^
<source>:43:15: note: in instantiation of function template specialization 'AllItemsAre<arr, &isLower>' requested here
   43 | static_assert(AllItemsAre<arr, &isLower>());
      |               ^
<source>:43:15: error: static assertion failed due to requirement 'AllItemsAre<arr, &isLower>()'
   43 | static_assert(AllItemsAre<arr, &isLower>());
      |               ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
  • MSVC prints:
<source>(10): error C2338: static_assert failed: 'F(Item)'
<source>(24): note: while evaluating constexpr function 'detail::AllItemsAre'
<source>(43): note: while evaluating constexpr function 'AllItemsAre'
<source>(10): note: the template instantiation context (the oldest one first) is
<source>(43): note: see reference to function template instantiation 'bool AllItemsAre<& arr,&bool isLower(std::string_view)>(void)' being compiled
<source>(24): note: see reference to function template instantiation 'bool detail::AllItemsAre<& arr,&bool isLower(std::string_view),0,1,2,3>(std::integer_sequence<size_t,0,1,2,3>)' being compiled
<source>(17): note: see reference to function template instantiation 'bool detail::checkItem<& arr[2],&bool isLower(std::string_view)>(void)' being compiled
<source>(43): error C2607: static assertion failed

Note for each compiler error logs contain arr[2] indicating on which item assertion has failed. So IMO this is a big win comparing to other similar answer(s).

I wonder if it is possible to improve it in such way that error message is more clear so it is easier to spot which item is a problem.

Comments

2

TLDR Getting inspiration from several answers, the final proposal at the bottom performs the static check and, in case of failure, gives a diagnosis indicating the first failing index for gcc, clang and msvc.


@Joel answer seems OK. Yet here is a solution valid for C++17 also:

#include <string_view>

using namespace std::string_view_literals;

constexpr std::string_view arr[] = {
    "ab"sv,
    "xyz"sv,
};

template <std::size_t... Is>
constexpr void CheckImpl(std::index_sequence<Is...>&&) {
    constexpr bool test = ((arr[Is] == "xyz") && ...);
    static_assert(test, "test failed");
};
constexpr auto Check() {
    using ind_t = std::make_index_sequence<std::size(arr)>;
    CheckImpl(ind_t{});
}

int main() {
    Check();
    return 0;
}

LIVE

@Joel comment on i being not usable in constant expression is accurate. Here the technics uses std::make_index_sequence to retrieve a compile-time list of index (in the form of the template non-type pack std::size_t... Is) and then use a fold expression to apply the test on all indices: ((arr[Is] == "xyz") && ...).

I'm nearly sure that they are now more elegant ways to do that but the technic is generic and can be used in many situations.


In the spirit of @Marek solution, CheckImpl can be modified to give the first failing index:

template <std::size_t... Is>
constexpr void CheckImpl(std::index_sequence<Is...>&&) {
    constexpr std::size_t i = []() {
        std::size_t n = 0;
        bool test = ((++n, arr[Is] == "xyz") && ...);
        return test?n:n-1;
    }();
    static_assert(i == sizeof...(Is), "test failed");
};

LIVE

In the fold expression I increment a counter, the folding will stop as soon as a test is failing.
If no test fails, n will be sizeof...(Is) and I'm returning it. Overwise it is the index of the failing element +1, so I'm returning n-1. A possible compiler (clang/gcc) output is:

note: the comparison reduces to '(0 == 2)'

Which indicates that elements 0 does not respect the assertion.

Unfortunatly, msvc is not that verbose.


Final solution using a single lambda as in @WeijunZhou answer and my solution to find the first failing index, improving the diagnosis by using a helper template struct that cannot be instantiated when the given array is invalid:

#include <iostream>
#include <string_view>

using namespace std::string_view_literals;

constexpr std::string_view arr[] = {
    "xyz"sv,
    "xyz"sv,
    "ab"sv,
    "xyz"sv,
};

// helper struct that fails to instantiate if Ind != N
template <std::size_t Ind, std::size_t N>
struct FailedAtIndForN {
    static_assert(N < 0);
};
// specialization for the case where the array test succeeded and Ind == N
template <std::size_t X>
struct FailedAtIndForN<X, X> {};

int main() {
    []<auto... Is>(std::index_sequence<Is...>) {
        constexpr std::size_t i = []() {
            std::size_t n = 0;
            bool test = ((++n, arr[Is] == "xyz") && ...);
            return test ? n : n - 1;
        }();
        [[maybe_unused]] static constexpr FailedAtIndForN<i, sizeof...(Is)>
            failure;
    }(std::make_index_sequence<std::size(arr)>());
    return 0;
}

LIVE As above, the possibly failing index is constant evaluated, using a nested lambda. Then I try to instantiate FailedAtIndForN which will fail if the returned index is not the size of the array.
In this case, in each tested compiler output there is, for instance, 'FailedAtIndForN<2, 4>', meaning that the array has a wrong value at index 2 for a total size of 4.

NB1: with c++26 it might be easier to use a static_assert with a constant expression as assertion message indicating a failure to instantiate. NB2: for the record, i is in fact not necessary (except for readability) as 'FailedAtIndForN` can take the immediately invoked lambda as first template argument LIVE.

Comments

1

Since i in your example is not constexpr but only const, it doesn't work. However you can make use of a constexpr or consteval function, where the function-local variables don't have to be constexpr but the whole function itself still evaluates during compile time.

#include <string_view>
#include <algorithm>
#include <ranges>

using namespace std::string_view_literals;

template <std::size_t N>
consteval bool arrContainsOnly(const std::string_view(&arr)[N], const std::string_view s) {
    for (const auto& i : arr) {
        if (i != s) {
            return false;
        }
    }
    return true;
}

int main(){
    constexpr std::string_view arr1[] = {"ab"sv, "xyz"sv,};
    constexpr std::string_view arr2[] = {"xyz"sv, "xyz"sv, "xyz"sv};

    static_assert(!arrContainsOnly(arr1, "xyz"sv));
    static_assert(arrContainsOnly(arr2, "xyz"sv));

    static_assert(std::all_of(std::begin(arr2), std::end(arr2),
                [](const std::string_view& i) { return i == "xyz"sv; }));
    static_assert(std::ranges::all_of(arr2,
                [](const std::string_view& i) { return i == "xyz"sv; }));

    return 0;
}

Live example (tested with c++20 using gcc, clang and msvc)

You can modify my arrContainsOnly() function to take other arguments or behave differently as long as it can be executed at compile time and is marked as constexpr/consteval.

1 Comment

It's a nice way for asserting about all the elements in the array. But it doesn't show how to perform a general constexpr operation on each element at compile time (which the OP asked about - they mentioned that the static_assert was only an example).
0

From a developer's point of view, it would be nice to write something like:

constexpr std::array<int,3> arr = {2,4,6};

int main()
{
    iterate (arr, [] <auto arg> ()  {  static_assert (arg%2 == 0);  });
}

where one can provide the array and a generic lambda that will take as input a non type template parameter arg corresponding to an item of the array.

The first attempt to do so would be to define a template iterate method like this:

template<typename ARRAY, typename FCT>
void iterate (ARRAY arr, FCT fct)
{
    [&]<auto... Is>(std::index_sequence<Is...>) 
    {
       (fct.template operator() <arr[Is]> (), ...);
    }(std::make_index_sequence<arr.size()>{});
}

However this cannot work since arr is not known as constexpr which would make fail a test implying static_assert.

The only way I can see to overcome this issue is to (unfortunately) use the preprocessor:

#define iterate(arr,fct)                            \
    [&]<auto... Is>(std::index_sequence<Is...>)     \
    {                                               \
       (fct.template operator() <arr[Is]> (), ...); \
    }(std::make_index_sequence<arr.size()>{}) 

Demo here.

However, this solution has some serious drawbacks since:

  1. it is not possible to use types that can't be used as non type template parameters
  2. the infamous usage of the preprocessor makes it impossible to write something like iterate (std::array<int,3> {2,4,6}, [] <auto arg> () { static_assert (arg%2 == 0); });

Nevertheless, if it would be possible to avoid preprocessor usage, it could be an interesting API for the developer.

Comments

0

After reading all your suggestions, my conclusions are:

  1. the simplest solution would be to use std::array<std:string_view> because it is constexpr-friendly and the operator==() can be used to compare 2 objects of this type:
constexpr std::array<std::string_view,2> inp = {/*...*/};
constexpr std::array<std::string_view,2> out = someConstexprFunc(inp);
constexpr std::array<std::string_view,2> ref = {/*...*/};

static_assert(out == ref);

It is simpler but doesn't provide information about the index causing an eventual mismatch

  1. if the index causing the mismatch is needed then replace operator==() with a constexpr function returning the index and use that index as template parameter for an error class with a meaningful name:
#include <algorithm>
#include <array>
#include <ranges>
#include <string_view>
using namespace std::string_view_literals;

consteval auto run(std::ranges::range auto const& range){
    std::remove_cvref_t<decltype(range)> res;
    std::transform(range.cbegin(), range.cend(), res.begin(), [](const std::string_view s){
        return s.substr(0, 1);
    });
    return res;
}

template<size_t N>
constexpr size_t cmp(const std::array<std::string_view,N>&a, const std::array<std::string_view,N>&b){
    for(size_t i=0; i<N; ++i){
        if(a[i] != b[i]){
            return i+1;
        }
    }
    return 0;
}

template<size_t index> struct CompareErrorAtIndex{
    static_assert(index == 0, "compare `out` with `ref`");
};

constexpr std::array<std::string_view,2> inp = {"ab"sv, "xyz"sv};
constexpr std::array<std::string_view,2> out = run(inp);
constexpr std::array<std::string_view,2> ref = {"a"sv, "x"sv,};

int main(){
    //static_assert(out == ref); //OK but no info about index generating the error

    constexpr auto index = cmp(out, ref);
    if constexpr(index){
        CompareErrorAtIndex<index>();
    }

    return index;
}

The error class together with its template argument (the index) will appear in the build output for all 3 main compilers (msvc, clang, gcc) like, for example: ... CompareErrorAtIndex<1> ...

You can see it in action at https://godbolt.org/z/z41TqhvPE

Thank you very much for your input! (I'm still open for comments, suggestions, improvements, constructive critics)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.