c++c++17preprocessorinitializerstdarray

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?


Solution

  • 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, std::size_t...Is>
    constexpr auto as_array_impl (std::initializer_list<T> l, std::index_sequence<Is...>)  {
        std::array<T,N> res {};
        std::size_t i=0;
        for (auto x : l)  { res[i++] = x; }
        return res;
    }
    
    template<int N,typename T>
    constexpr auto as_array (std::initializer_list<T> l)  {
        return as_array_impl<N,T> (l, std::make_index_sequence<N>());
    }
    

    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