12

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?

1
  • 1
    @ThomasWeller indeed. But that means I should compute the number of available dependencies separately, right? Commented Jul 1 at 12:45

7 Answers 7

14

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

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

1 Comment

Ooh, using macros to sneak around parameters not being usable in constant expressions, that's smart. Handily gets around l.size() being invalid within ar_array() itself, and forces it to be constexpr by making it a template parameter instead. This is a perfectly valid use of macros, there's nothing to be ashamed of here. 👍
10

You could create some helper functions similar to std::to_array and add a dummy entry last when creating the array (which will not be included in the final std::array):

// populate the std::array with all except the last entry in the C array:
template <class T, size_t... Is>
constexpr std::array<T, sizeof...(Is)> copier(const T (&arr)[sizeof...(Is) + 1],
                                              std::index_sequence<Is...>) {
    return {{arr[Is]...}};
}

// populate the std::array with all except the last entry in the C array:
template <class T, size_t... Is>
constexpr std::array<T, sizeof...(Is)> mover(T (&&arr)[sizeof...(Is) + 1],
                                             std::index_sequence<Is...>) {
    return {{std::move(arr[Is])...}};
}

// overload for lvalues
template <class T, std::size_t N>
constexpr auto mk_array_minus_last(const T (&arr)[N]) {
    return copier(arr, std::make_index_sequence<N - 1>{});
}

// overload for rvalues
template <class T, std::size_t N>
constexpr auto mk_array_minus_last(T (&&arr)[N]) {
    return mover(std::move(arr), std::make_index_sequence<N - 1>{});
}

Then creating available_dependencies would be similar to that of using std::to_array:

constexpr static auto available_dependencies = mk_array_minus_last<literal>({
#ifdef HAS_DEPENDENCY1
    "dependency 1",
#endif
#ifdef HAS_DEPENDENCY2
    "dependency 2",
#endif
    "dummy"}); // will not be included in the final std::array

Demo

Comments

7

You could add a "dummy" entry that will avoid issues with potentially initial comma and also allows template type deduction, e.g.

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

then you can afterwards remove this dummy entry

using arrtype = typename decltype(available_dependencies_tmp)::value_type;
constexpr std::size_t arrsize = available_dependencies_tmp.size();

template <std::size_t ... Is>
constexpr auto remove_first (std::index_sequence<Is...>) {
    return std::array <arrtype,sizeof...(Is)>{ available_dependencies_tmp[Is+1]... };
}

constexpr static auto available_dependencies = remove_first (
    std::make_index_sequence<arrsize-1>() 
);

Demo

Note: the intermediate remove_first is required because of c++17 requirement. With c++20, available_dependencies could be defined on the fly from a template lambda.

2 Comments

Trailing comma in initialiser list is not a problem (they work just fine), empty initialiser is (if there are no dependencies). But it does solve the problem, +1
@Yksisarvinen, you're right, this is about the initial alone comma (updated the answer)
5

If you really want to be able to initialize from strings (string_views) only then you can do it like this.

#include <array>
#include <iostream>

#define HAS_DEPENDENCY1

struct no_config {}; 
constexpr auto create_array_of_literals(no_config) 
{ 
    return std::array<std::string_view, 0>{}; 
}

template<std::size_t N>
constexpr auto create_array_of_literals(std::string_view (&&literals)[N])
{
    std::array<std::string_view, N> result{};
    //std::copy(literals, literals + N, result.begin());
    for(std::size_t n{0}; n<N; ++n ) result[n] = literals[n];
    return result;
}

constexpr auto literals = create_array_of_literals({
     "always_there_dependency",
#ifdef HAS_DEPENDENCY1
    "dependency 1",
#endif
#ifdef HAS_DEPENDENCY2
    "dependency 2",
#endif
});

constexpr auto no_literals = create_array_of_literals({});

int main()
{
    for (const auto& literal : literals)
    {
        std::cout <<  literal << "\n";
    }
}

Comments

1

if no dependency is available (which can happen), the compiler cannot deduce the template arguments of std::array (the array is empty).

Since you know what the template arguments should be for no dependencies, you could create a custom type with a deduction guide that also covers the empty case:

template<std::size_t S>
struct literal_array : std::array<literal, S> {};
template<typename... Lits>
literal_array(Lits&&...) -> literal_array<sizeof...(Lits)>;

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

Or, if you're uncomfortable with deriving from std::array<...> or want available_dependencies to have exactly the type std::array<...> :

template<std::size_t S>
struct literals {
    std::array<literal, S> array;
};
template<typename... Lits>
literals(Lits&&...) -> literals<sizeof...(Lits)>;

constexpr static auto available_dependencies = literals{
#ifdef HAS_DEPENDENCY1
    "dependency 1",
#endif
#ifdef HAS_DEPENDENCY2
    "dependency 2",
#endif
}.array;

if dependency 1 is available but dependency 2 isn't, the code doesn't compile because of the trailing comma.

Both examples use aggregate initialization, which allows trailing commas.

Demo

Comments

0

MAaybe somethig like this

#include <array>
#include <string_view>
#include <cstddef>
#include <iostream>
#include <cassert>

using sv = std::string_view;

//------------------------------------------------------------------------------
// 0) Define your dependency macros here.
//    Comment out any you don't want included.
#define HAS_DEPENDENCY1  1
#define HAS_DEPENDENCY2  1
#define HAS_DEPENDENCY3  1
#define HAS_DEPENDENCY4  1
#define HAS_DEPENDENCY5  1

//------------------------------------------------------------------------------
// 1) X-macro list of all possible dependencies:
//    Add new lines for each additional dependency.
#define DEPENDENCY_LIST           \
    X("dependency 1", HAS_DEPENDENCY1) \
    X("dependency 2", HAS_DEPENDENCY2) \
    X("dependency 3", HAS_DEPENDENCY3) \
    X("dependency 4", HAS_DEPENDENCY4) \
    X("dependency 5", HAS_DEPENDENCY5)

//------------------------------------------------------------------------------
// 2) Count how many are enabled:
constexpr std::size_t count_deps() {
    std::size_t c = 0;
    #define X(name, flag) if constexpr (bool(flag)) ++c;
    DEPENDENCY_LIST
    #undef X
    return c;
}

//------------------------------------------------------------------------------
// 3) Build the constexpr std::array of enabled dependencies:
constexpr auto make_available_deps() {
    constexpr std::size_t N = count_deps();
    std::array<sv, N> a{};
    std::size_t idx = 0;
    #define X(name, flag) \
        if constexpr (bool(flag)) \
            a[idx++] = sv{name};
    DEPENDENCY_LIST
    #undef X
    return a;
}

//------------------------------------------------------------------------------
// 4) Expose the final array:
constexpr auto available_dependencies = make_available_deps();

//------------------------------------------------------------------------------
// 5) Compile-time tests:
static_assert(count_deps() == available_dependencies.size(),
              "count_deps() must match available_dependencies.size()");

// Build expected array for enabled macros:
constexpr auto build_expected() {
    constexpr std::size_t N = count_deps();
    std::array<sv, N> e{};
    std::size_t i = 0;
    #define X(name, flag) \
        if constexpr (bool(flag)) \
            e[i++] = sv{name};
    DEPENDENCY_LIST
    #undef X
    return e;
}
constexpr auto expected = build_expected();

static_assert(available_dependencies == expected,
              "available_dependencies must exactly match the enabled macros");

//------------------------------------------------------------------------------
// 6) Runtime smoke-test:
int main() {
    std::cout << "Detected " << available_dependencies.size() << " dependencies:\n";
    for (auto &dep : available_dependencies) {
        std::cout << " - " << dep << "\n";
    }
    std::cout << "All tests passed.\n";
    return 0;
}

2 Comments

Whatever you do in C++ if you can avoid macros you should do so
Oh no, not that X macro hack again. Please STOP that.
0

If you specify the type as the first argument using macros (option 1) or using a tag type (option 2), you don't run into issues with trailing commas:

#include <array>
#include <string>

#define make_array(type, ...) imake_array<type>(__VA_ARGS__);

template <typename T, typename... U>
constexpr auto imake_array(U... elems)
{
    return std::array<T, sizeof...(U)>{elems...};
}

constexpr std::array data = make_array(const char*
);
constexpr std::array data2 = make_array(const char*
    ,"foo"
    ,"bar"
);

int main()
{
    return data2.size();
}

Without macros:

#include <array>
#include <string>

template <typename T>
struct tag_t{};

template <typename T>
constexpr tag_t tag = tag_t<T>{};

template <typename T, typename... U>
constexpr auto make_array(tag_t<T>, U... elems)
{
    return std::array<T, sizeof...(U)>{elems...};
}

constexpr std::array data = make_array(tag<const char*>
);
constexpr std::array data2 = make_array(tag<const char*>
    ,"foo"
    ,"bar"
);

int main()
{
    return data2.size();
}

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.