17

I need an "elegant" way of initializing a vector in the declaration phase with the content of another one and a few extra elements.

What I want to solve is:

Let's consider the following (example) declaration with initialization:

const std::vector<std::string> c90_types = {
    "char",
    "signed char",
    "unsigned char",
    "short",
    "unsigned short",
    "int",
    "unsigned int",
    "long",
    "unsigned long",
    "float",
    "double",
    "long double"
};

const std::vector<std::string> c99_types = {
    "char",
    "signed char",
    "unsigned char",
    "short",
    "unsigned short",
    "int",
    "unsigned int",
    "long",
    "unsigned long",
    "float",
    "double",
    "long double",
    "long long",
    "unsigned long long",
    "intmax_t",
    "uintmax_t"
};

as you can see c99_types has a subset which is exactly c90_types. I want to avoid the situation where I need to change the subset and then change manually the "superset" too, just to avoid the extra step that might introduce bugs :)

As a side note, I don't want to write code like:

second.insert(second.begin(), first.begin(), first.end());
second.push_back(something);

Any good and clean solutions to this?

Ferenc Deak
  • 34,348
  • 17
  • 99
  • 167
  • You can use the copy constructor instead of `insert`. –  Mar 10 '15 at 12:36
  • 1
    @райтфолд No I don't want to do that, because: 1. I want the vector to be const, 2. I don't want to write extra code for the initialization. – Ferenc Deak Mar 10 '15 at 12:37
  • Do you *have* to use `std::vector`? Could you use `std::array`? – Cornstalks Mar 10 '15 at 12:42
  • @Cornstalks `std::array` is fine too but I don't like that I have to specify the size of it, since it will contain a lot of elements and counting them by hand ... hmm... – Ferenc Deak Mar 10 '15 at 12:44
  • @fritzone copy ctor takes by ref-to-const. –  Mar 10 '15 at 13:26
  • @fritzone: I agree, which is why I provided a `make_array` function in my answer, which if combined with `auto`, allows you to omit the size of the array. – Cornstalks Mar 10 '15 at 13:50
  • @райтфолд Yup, that's correct :) – Ferenc Deak Mar 10 '15 at 14:02
  • What is the hindrance to writing `sec.insert(sec.begin(), first.begin(), first.end());` or simply using "std::copy` or `operator=`, followed by back-inserting? You said you don't want that, but why? If that is, after all, just what you're doing. If you properly reserve beforehand (which one should always do anyway), this is very efficient. – Damon Mar 10 '15 at 14:02
  • @Damon That's simple: Firstly: In the real life situation there are more than one vectors, and I build more than one "final" vectors with a combination of those basic building block vectors. Secondly, I don't want to expose a function doing this operation, someone by mistake might call it. Thirdly: As mentioned below, there are >1000 elements, maintaining them manually in two places would be painful. – Ferenc Deak Mar 10 '15 at 14:06
  • I would consider using a master array of possible data and making the other arrays point to those elements, or use shared pointers some how, otherwise you will duplicate this data in memory many times. If they are strings, you could also store `const char *` and rely on string pooling optimization. – Neil Kirk Mar 10 '15 at 14:18
  • There's a c++1y solution based on `tuple_cat` that you might find interesting (the example works with integers, but it's kind of irrelevant what type you put into the array): http://stackoverflow.com/a/25089095/572743 – Damon Mar 10 '15 at 14:45

5 Answers5

18

There is a trick called "I want to initialize a const variable with something elaborate." that became possible with C++11, shamelessly stolen from Javascript.

const std::vector<std::string> c90_types = {
    "char",
    // and so on, and so forth....
};

const std::vector<std::string> c99_types = ([&](){
    const auto additional_types = { // initializer_list<const char *>, but it does not matter.
        "long long",
        "unsigned long long",
        "intmax_t",
        "uintmax_t"
    };

    std::vector<std::string> vec{c90_types};

    vec.insert(vec.end(), additional_types.begin(), additional_types.end());

    return vec;
})();

Pack your initialization logic into an unnamed lambda, and call it right away, copy-initializing your const variable.

vec is moved, not copied.

Laurent LA RIZZA
  • 2,905
  • 1
  • 23
  • 41
7

You could define the biggest vector (here that would be c99_types) first, and then construct the others with iterators from the largest one.

Here is an example :

const vector<int> a{1,2,3,4};
const vector<int> b{begin(a), begin(a)+2}; // b is {1,2}

So you could write:

const std::vector<std::string> c99_types = {
    "char",
    "signed char",
    "unsigned char",
    "short",
    "unsigned short",
    "int",
    "unsigned int",
    "long",
    "unsigned long",
    "float",
    "double",
    "long double",
    "long long",
    "unsigned long long",
    "intmax_t",
    "uintmax_t"
};

const std::vector<std::string> c90_types{begin(c99_types), begin(c99_types)+12};
tux3
  • 7,171
  • 6
  • 39
  • 51
  • In this case, better make sure the bigger set isn't, say, alphabetized later on without the person realizing the positions of those elements are important. – chris Mar 10 '15 at 12:49
  • @chis Yes, they need to have the same order. – tux3 Mar 10 '15 at 12:50
  • 2
    Yes, identifying that 12 is my biggest fear, because these vectors will be pretty large (>1000 elements) and there will be constant changes to them all the time. And of course there are not only 2 vectors... – Ferenc Deak Mar 10 '15 at 12:55
5

Option 1: std::array

This can probably be cleaned up and improved a lot, but it's at least a starting point (it uses Jonathan Wakely's redi::index_tuple:

template<typename T, std::size_t N, unsigned... I, typename ...U>
inline auto
append_array_helper(const std::array<T, N>& array, redi::index_tuple<I...>, U&&... elements) -> std::array<T, N + sizeof...(elements)>
{
    return std::array<T, N + sizeof...(elements)>{ std::get<I>(array)..., std::forward<U>(elements)... };
}

template<typename T, std::size_t N, typename ...U>
inline auto
append_array(const std::array<T, N>& array, U&&... elements) -> std::array<T, N + sizeof...(elements)>
{
    return append_array_helper(array, typename redi::make_index_tuple<N>::type(), std::forward<U>(elements)...);
}

const std::array<std::string, 12> c90_types = {
    "char",
    "signed char",
    "unsigned char",
    "short",
    "unsigned short",
    "int",
    "unsigned int",
    "long",
    "unsigned long",
    "float",
    "double",
    "long double"
};

const std::array<std::string, 16> c99_types = append_array(
    c90_types,
    "long long",
    "unsigned long long",
    "intmax_t",
    "uintmax_t"
);

If you don't want to specify the array size, you can use the following method:

template<typename T, typename... U>
constexpr auto make_array(U&&... elements) -> std::array<T, sizeof...(elements)>
{
    return { std::forward<U>(elements)... };
}

const auto c90_types = make_array<std::string>(
    "char",
    "signed char",
    "unsigned char",
    "short",
    "unsigned short",
    "int",
    "unsigned int",
    "long",
    "unsigned long",
    "float",
    "double",
    "long double"
);

...

Option 2: Macros

Not my favorite, but it's simple and easy to understand and edit:

#define C90_TYPES         \
    "char",               \
    "signed char",        \
    "unsigned char",      \
    "short",              \
    "unsigned short",     \
    "int",                \
    "unsigned int",       \
    "long",               \
    "unsigned long",      \
    "float",              \
    "double",             \
    "long double"

#define C99_TYPES         \
    C90_TYPES,            \
    "long long",          \
    "unsigned long long", \
    "intmax_t",           \
    "uintmax_t"

const std::vector<std::string> c90_types = {
    C90_TYPES
};

const std::vector<std::string> c99_types = {
    C99_TYPES
};
Cornstalks
  • 37,137
  • 18
  • 79
  • 144
4

You can use boost::join:

#include <vector>
#include <boost/range/join.hpp>

const std::vector<std::string> c90_types = {
  "char",
  "signed char",
  "unsigned char",
  "short",
  "unsigned short",
  "int",
  "unsigned int",
  "long",
  "unsigned long",
  "float",
  "double",
  "long double"
};


auto range = boost::join(c90_types, std::vector<std::string>{
    "long long",
    "unsigned long long",
    "intmax_t",
    "uintmax_t"
});

const std::vector<std::string> c99_types(range.begin(), range.end());
Drax
  • 12,682
  • 7
  • 45
  • 85
  • Note that this defines **3** global variables. And unfortunately the return type of `boost::join` isn't documented to have a conversion to container types. – MSalters Mar 11 '15 at 15:32
  • @MSalters Yep it is not convertible that's why i needed the temporary range variable, hopefully if/when ranges will be added to C++17, containers will be able to be built from an `std::range`. We can always use a detail namespace or whatever trick we prefer to hide to hide that temporary variable meanwhile :) – Drax Mar 11 '15 at 17:52
  • I was trying to point out it's in fact not temporary at all, since it's a global. Temporaries have a lifetime to the end of the full expression, globals live forever. – MSalters Mar 11 '15 at 20:51
3

With extra code, you may still have const vector:

std::vector<std::string> make_c99_type()
{
    auto res = c90_types;
    const std::vector<std::string> extra_c99_types = {
        "long long",
        "unsigned long long",
        "intmax_t",
        "uintmax_t"
    };
    res.insert(res.end(), extra_c99_types.begin(), extra_c99_types.end());
    return res;
}

const std::vector<std::string> c99_types = make_c99_type();
Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • 3
    Seems like a good fit for an immediately invoked lambda if you ask me. – chris Mar 10 '15 at 12:47
  • 2
    Given the comment on the other answer, I take that back. A function taking an initializer list and a smaller vector would be more reusable. `auto c99_types = appended(c90_types, {...});`. – chris Mar 10 '15 at 13:01