Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Construct an std::array at compile time in C++17 using the preprocessor

I am developing a C++17 framework that has optional third-party dependencies and I'd like to construct an std::array that contains the names of the available dependencies.

The CMake file sets preprocessor definitions such as HAS_DEPENDENCY1, HAS_DEPENDENCY2, ..., so writing such a function with an std::vector is quite easy:

using literal = const char*;

std::vector<literal> available_dependencies() {
   std::vector<literal> dependencies{};
#ifdef HAS_DEPENDENCY1
   dependencies.emplace_back("dependency 1");
#endif
#ifdef HAS_DEPENDENCY2
   dependencies.emplace_back("dependency 2");
#endif
   return dependencies;
}

I'd like to have a compile-time, constexpr equivalent of that. I tried this:

constexpr static std::array available_dependencies{
#ifdef HAS_DEPENDENCY1
    "dependency 1",
#endif
#ifdef HAS_DEPENDENCY2
    "dependency 2"
#endif
};

but there are two issues:

  1. if no dependency is available (which can happen), the compiler cannot deduce the template arguments of std::array (the array is empty).
  2. if dependency 1 is available but dependency 2 isn't, the code doesn't compile because of the trailing comma.

I managed to address the first issue with something like:

template <typename... Literals>
constexpr std::array<literal, sizeof...(Literals)> make_literal_array(Literals&&... literals) {
   return {literals...};
}

but this doesn't support trailing commas. I tried with an std::initializer_list (they allow trailing commas), but couldn't get it to work.

Perhaps you'll be more inspired than me?

like image 957
Charlie Vanaret - the Uno guy Avatar asked Mar 03 '26 04:03

Charlie Vanaret - the Uno guy


1 Answers

Here is another proposal that uses the fact that initializer lists can cope with trailing commas. Since it is uneasy to directly use std::array in your context, why not defining a std::initializer_list instead (setting the type but not the number of items)

constexpr static std::initializer_list<const char*> available_dependencies_list = {
#ifdef HAS_DEPENDENCY1
    "dependency 1",
#endif
#ifdef HAS_DEPENDENCY2
   "dependency 2",
#endif
};

You can iterate it like in for-range loop

for (auto x : available_dependencies_list) {
    std::cout << x << "\n"; 
}

If you want to have a std::array, you can have one with the following

template<int N,typename T>
constexpr auto as_array (std::initializer_list<T> l)  {
    std::array<T,N> res {};
    std::size_t i=0;
    for (auto x : l)  { res[i++] = x; }
    return res;
}

which can be used e.g.

constexpr auto available_dependencies = as_array <available_dependencies_list.size()> (available_dependencies_list);

The ugly fact here is that one has to provide the size as a template parameter (but in c++17 we can't do the same for the list itself), so one has some kind of redundancy. We can still (shamelessly) use a macro such as

#define AS_ARRAY(l)  as_array<l.size()>(l);
constexpr auto available_dependencies = AS_ARRAY(available_dependencies_list);

Demo


Update

According to DavidG's comment, there was a useless std::make_index_sequence<N>() in the answer, so I updated the answer above which makes the code simpler.

Moreover, I have observed an issue with clang when using const char* (no issue with g++ and msvc though), e.g.

error: static assertion expression is not an integral constant expression
static_assert (available_dependencies[0] == "dependency 1");

Using std::string_view instead of const char* for the template parameter of the std::initializer_list makes it work for the 3 compilers

Demo

like image 76
abcdefg Avatar answered Mar 05 '26 19:03

abcdefg