Tomáš Zato
Tomáš Zato

Reputation: 53236

Initialize array of compile time defined size as constant expression

I have an array of strings that must be allocated once and their underlying c_str must remain valid for entire duration of the program.

There's some API that provides info about some arbitrary data types. Could look like this:

// Defined outside my code
#define NUMBER_OF_TYPES 23
const char* getTypeSuffix(int index);

The getTypeSuffix is not constexpr, so this must work at least partially in runtime.

The interface that I must provide:

// Returned pointer must statically allocated (not on stack, not malloc)
const char* getReadableTypeName(int type);

Now my array should have following type:

std::string typeNames[NUMBER_OF_TYPES];

For my purposes, it will be initialized within a wrapper class, right in the constructor:

class MyNames
{
  MyNames()
  {
    for (int i = 0; i < NUMBER_OF_TYPES; ++i)
    {
      names[i] = std::string("Type ") + getTypeSuffix(i);
    }
  }

  const char* operator[](int type) { return _names[(int)type].c_str(); }

private:
  std::string _names[NUMBER_OF_TYPES];
};

This is then used in an singleton-ish kind of way, for example:

const char* getReadableTypeName(int type) 
{
  static MyNames names;
  return names[type];
}

Now what I want to improve is that I can see that the for loop in the constructor could be replaced as such:

 MyNames() : _names{std::string("Type ") + getTypeSuffix(0), std::string("Type ") + getTypeSuffix(1), ... , std::string("Type ") + getTypeSuffix(NUMBER_OF_TYPES-1)}
 {}

Obviously a pseudocode, but you get the point - the array can be initialized directly, leaving the constructor without body, which is neat. It also means that the array member _names can be const, further enforcing the correct usage of this helper class.

I'm quite sure there would be many other uses to filling an array by expressions in compile time, instead of having loop. I would even suspect that this is something that happens anyway during 03.

Is there a way to write a C++11 style array initializer list that has flexible length and is defined by an expression? Another simple example would be:

constexpr int numberCount = 10;
std::string numbers[] = {std::to_string(1), std::to_string(2), ... , std::to_string(numberCount)};

Again, an expression instead of a loop.

I'm not asking this question because I was trying to drastically improve performance, but because I want to learn about new, neat, features of C++14 and later.

Upvotes: 2

Views: 351

Answers (4)

Max Langhof
Max Langhof

Reputation: 23701

Since you ache to use new features, let's use range-v3 (soon-to-be the ranges library in C++2a) to write some really short code:

const char* getReadableTypeName(int type) 
{
    static const std::vector<std::string> names =
        view::ints(0, 23) | view::transform([](int i) {
            return "Type " + std::to_string(i);
        });
    return names[type].c_str();
}

https://godbolt.org/z/UVoENh

Upvotes: 1

Barry
Barry

Reputation: 303357

You can defer to an initialization function:

std::array<std::string, NUMBER_OF_TYPES> initializeNames()
{
    std::array<std::string, NUMBER_OF_TYPES> names;
    for (int i = 0; i < NUMBER_OF_TYPES; ++i) {
        names[i] = std::string("Type ") + getTypeSuffix(i);
    }
    return names;
}

const char* getReadableTypeName(int type) 
{
  static auto const names = initializeNames();
  return names[type].c_str();
}

which can be an immediately invoked lambda:

static auto const names = []{
    std::array<std::string, NUMBER_OF_TYPES> names;
    // ...
    return names;
}();

or do you really need the array requirement? We're making strings anyway so I don't understand, then you can just use range-v3:

char const* getReadableTypeName(int type) {
    static auto const names =
        view::iota(0, NUMBER_OF_TYPES)
        | view::transform([](int i){ return "Type "s + getTypeSuffix(i); })
        | ranges::to<std::vector>();
    return names[type].c_str():
}

Upvotes: 3

Artyer
Artyer

Reputation: 40891

You can use std::make_integer_sequence and a delegating constructor in C++14 (Implementations of std::make_integer_sequence exist in C++11, so this is not really C++14 specific) to get a template parameter pack of integers

#include <string>
#include <utility>

#define NUMBER_OF_TYPES 23

const char* getTypeSuffix(int index);

class MyNames
{
  MyNames() : MyNames(std::make_integer_sequence<int, NUMBER_OF_TYPES>{}) {}

  template<int... Indices>
  MyNames(std::integer_sequence<int, Indices...>) : _names{ (std::string("Type ") + getTypeSuffix(Indices))... } {}

  const char* operator[](int type) { return _names[(int)type].c_str(); }

private:
  const std::string _names[NUMBER_OF_TYPES];
};

This means that no strings are being default constructed.

Upvotes: 2

Jarod42
Jarod42

Reputation: 217810

instead of C-array use std::array, then you might write your function to return that std::array and your member can then be const:

std::array<std::string, NUMBER_OF_TYPES> build_names()
{
    std::array<std::string, NUMBER_OF_TYPES> names;
    for (int i = 0; i < NUMBER_OF_TYPES; ++i)
    {
          names[i] = std::string("Type ") + getTypeSuffix(i);
    }
    return names;
}


class MyNames
{
  MyNames() : _names(build_names()) {}
  const char* operator[](int type) const { return _names[(int)type].c_str(); }

private:
  const std::array<std::string, NUMBER_OF_TYPES> _names;
};

Now you have std::array, you might use variadic template instead of loop, something like (std::index_sequence stuff is C++14, but can be implemented in C++11):

template <std::size_t ... Is> 
std::array<std::string, sizeof...(Is)> build_names(std::index_sequence<Is...>)
{
     return {{ std::string("Type ") + getTypeSuffix(i) }};
}

and then call it:

MyNames() : _names(build_names(std::make_index_sequence<NUMBER_OF_TYPES>())) {}

Upvotes: 6

Related Questions