How can I initialize this array at compile time in the constructor?

Here is a simplified version of the code I am having an issue with:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
constexpr NUMLABS = 8; //Should be able to be changed
class Labs
{
    public:
        Labs(int lab_number) {...}
}

class Database
{
    public:
        Database() : labs{1,2,3,4,5,6,7,8}
    private:
        Labs labs[NUMLABS];
}


As you can see, I initialize the labs member with a comma-seperated index sequence at compile time. However, this is hardcoded in, and I don't want that. I want to be able to just change NUMLABS to some arbitrary value (lets say 100) and I want to be able to generate an index sequence at compile time that will output 1, 2, 3, ..., NUMLABS-1, NUMLABS. Can someone point me in a direction of how I can create a template metaprogram that does this? Maybe there already is one in the standard library that I could use?

NOTE: Please do not ask me to change the C-style array to an std::array. I have a reason for using a C-style array, and I want to be able to generate it using the C-style array.
Last edited on
array sizes must be known at compile time, though many compilers support run time to one degree or another, you can't do that in c++ officially.

index sequence can be run using std::iota.
a variable length 'array' can be done with a vector (I know you don't want to hear this).

to do it correctly, using a C array, you need to make the C-array have a 'biggest possible size' and a size variable for how many slots are used. At that point you can fill it up, if that approach is ok. Or you can use a pointer, and allocate what you want. Pointer 'arrays' and C arrays are more or less fully interchangeable.

what special reason do you have to use the C array? You can lift &vec[0] as a C-array if careful. If the vector is resized, the pointer can be invalidated, see the iterator rules on this behavior.
Last edited on
jonnin wrote:
array sizes must be known at compile time, though many compilers support run time to one degree or another, you can't do that in c++ officially.


I want the expansion to happen at compile time. Not at run time. The "comma expression" should be expanded into the ctor-initializer when the program is compiled. NUMLABS is a compile time constant that is changed by the programmer (in my case, my instructor will be changing this constant to various different values and recompiling in order to test the program). This is why I was asking if there was a way to do this using template metaprogramming, or perhaps a preprocessor macro expansion.

Actually, I just recently find a way to accomplish what I am seeking. Could you please take a look at this code and see if this is the best possible way to do it?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
constexpr NUMLABS = 8; //Should be able to be changed
class Labs
{
    public:
        Labs(int lab_number) {/*...*/}
}

class Database
{
    public:
        template<int... Args>
        Database(std::integer_sequence<int, Args...> const&) : labs{(args+1)...} {}
    private:
        Labs labs[NUMLABS];
}

int main()
{
    Database a(std::make_integer_sequence<int, NUMLABS> {});
    //...
}


This seems to work fine. Is there a better way to do this or is this the best way?

Also, the reason I am not using containers like vector or std::array is because my instructor has explicitly forbidden them for this particular assignment. Only C-style arrays are to be used.


Last edited on
Database a(std::make_integer_sequence<int, NUMLABS>);
that's a function declaration, use curly braces instead
Database a{std::make_integer_sequence<int, NUMLABS>{}};


> my instructor has explicitly forbidden them for this particular assignment.
¿what's the purpose of the assignment?

I think it's a nice solution, as long as NUMLABS remains small (try with 100 000)
but fail to see what problem are you solving


(perhaps it's just resistance to change, I don't like at all this kind of templates uses)
Thanks for that correction. I had it in my original code but forgot to add the curly brackets when retyping it on this website.

The purpose of the assignment is to create a working computer lab database. Basically, you are in charge of creating NUMLABS labs (which is by default 8), and each lab has a certain number of computers that is given in the rubric (Lab 1 should have 19 computer, Lab 2 should have 37). These are to be defined as global constants.

My implementation basically abstracts a "computer" as a Station class. The Database class will represent the entire database. It stores a C-style array of NUMLABS Lab objects (as per the assignment requirements). Each Lab object stores a pointer to an array of Station objects (I use a unique_ptr here) and dynamically allocates in the ctor-initializer based on the predefined number of computers each lab is supposed to have as mentioned above.

The thing is, my instructor is going to change the NUMLABS value to 9, or perhaps 10, and add in entries for the size of those additional labs. It is expected that the program should still work based just changing the global constants alone.

The purpose of forbidding std::vector and such is to familiarize students with using raw arrays, or so I'm told. I don't really know or care. All I know is this is how it was stated we should do it, so I am going to follow those instructions.

ALSO: The reason I omitted these details is because they aren't relevant to the problem I am asking. Whenever I ask a question, I usually simplify my code to the exact problem that needs to be solved and omit all other unneccesary details like about the assignment and other implementation details in order to be able to get a better response.
Last edited on
Is line 12 verbatim to what you have in your actual code? If not, could you post just that line? I'm just trying to get your example to compile; I barely ever use this template-metaprogramming stuff in practice.

(You could still do all the stuff you mentioned above without doing the meta-template programming, it just means you'd being using a for loop to set each index.)
Last edited on
Yes, that line is verbatim. Here is a portion of my code. This should run and compile :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    constexpr size_t NUMLABS = 8;
    constexpr size_t LABSIZES[NUMLABS] = {19, 15, 24, 33, 61, 17, 55, 37};

    class Lab
    {
        public:
            Lab(size_t num) {}
    };

    class Database
    {
        public:
            template<int... args>
             Database(std::integer_sequence<int, args...> const&)
                            : labs{(args+1)...} {}

        private:
            //Our labs
            Lab labs[NUMLABS];

    };
    


    int main()
    {
            Database a(std::make_integer_sequence<int, NUMLABS>{});
    }


I got rid of any unnecessary member functions.
Last edited on
Ganado wrote:
(You could still do all the stuff you mentioned above without doing the meta-template programming, it just means you'd being using a for loop to set each index.)


Well no. That would require Lab to have a default constructor. I think I failed to mention that Lab is NOT DefaultConstructable. I need to do it in the ctor-initializer, and thus I cannot use a for loop
Last edited on
Thanks, that fixed it. You had a capital args before.

Edit: Replying to your 2nd post: Okay, I see what you mean now.
Last edited on
If the compiler optimizer doesn't optimize the naive code sufficiently, did you try constexpr before reaching for templates?

I don't like at all this kind of templates uses

From a practical standpoint, I agree. This is overkill - but a good exercise.
Last edited on

If the compiler optimizer doesn't optimize the naive code sufficiently, did you try constexpr before reaching for templates?


Could you explain what you mean by that?
I missed a relevant detail, so please disregard that.
Last edited on
(Nearly) the same question is asked here:
https://stackoverflow.com/questions/4754763/object-array-initialization-without-default-constructor

The most voted solution is not simpler than yours, if it works.
Seems like an array of some union-like class type (e.g. std::optional<lab>) could have promise here.

std::optional<T>
does need to check whether it owns a T before accessing it, but a similar class could sacrifice that guarantee if the user knew that every object in the array would always be initialized later.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <utility>
#include <new>

struct lab { explicit lab (int) {} };

template <typename T> struct construction_deferred
{
    constexpr construction_deferred()
      : dummy_() 
    {};
    constexpr ~construction_deferred() { value_.~T(); }
    
    // complicated constructor stuff isn't copied down here
    // just read the reference page for std::optional if desired
    
    // NOTE(mbozzi): A nontrivial union-like class has deleted special 
    // member functions by default
    
    constexpr T&       operator*() &       { return value_; }
    constexpr T const& operator*() const & { return value_; }
    constexpr T&&      operator*() &&      { return std::move(value_); }
    
    constexpr T*       operator->()       { return ptr_value(); }
    constexpr T const* operator->() const { return ptr_value(); }

    template <typename... Args> constexpr void construct(Args&&... args) 
    { new (ptr_value()) T(std::forward<Args>(args)...); }

private:
    constexpr T*       ptr_value()       { return std::addressof(value_); }
    constexpr T const* ptr_value() const { return std::addressof(value_); }

    
    struct empty {}; 
    union 
    {
        empty dummy_{}; // Empty object in the union doesn't prevent EBO
        T     value_; 
    };
};

struct database 
{
    database()
    {
        for (int i = 0; i < 10; ++i) labs[i].construct(i + 1);
        // later: use like std::optional<lab>
    }
    
private:
    construction_deferred<lab> labs[10];
};


While ostensibly this doesn't write the initializer at compile time, the optimizer ought to take care of the rest for you. Compilers are quite happy to unroll small loops without user intervention. For instance, given the following contrived example, GCC produces the exact same output as the template metaprogram above:
https://godbolt.org/z/ZXph-v
Last edited on
Topic archived. No new replies allowed.