Home > Net >  Using constexpr in class constructor with a fixed length array but declared at instantiation in C
Using constexpr in class constructor with a fixed length array but declared at instantiation in C

Time:07-28

I'm looking to instantiate a class with a constant member animalList that is (or points to) an array of another type Animal. My goal is to have all of these created and evaluated at compile time, which is why I'm using const and constexpr.

I think a pointer to a vector would work in this situation, but there is so much data that the extra overhead vectors would take up is not worth it, as far as I know. Plus, I don't need arrays to be of variable length after declaration. They will be fixed and constant, but they will be different for every class instance.

So having fixed-length arrays would make the most sense, but each instance will have a different length, and the compiler doesn't allow arrays to be declared without a fixed length specified in the class definition. My thought process was that if animalList could not be defined without a length, at least I would be able to specify a pointer to a fixed-length array of an unknown length.

In my head this would work:

class Animal {
  // ...
};

class Continent {
  public:
    const Animal[]* animalList; // animalList is of type pointer to array of animals
    constexpr Continent(Animal[]* animalsOnContinent) : animalList(animalsOnContinent) {}
};

But Animal[]* is not a valid type

CodePudding user response:

What you're trying to do is possible from C 20 onwards.

But first things first, const Animal[]* animalList; is not valid C syntax. The correct syntax for declaring animalList as a pointer to an array of unknown bound with elements of type Animal would be Animal (*animalList)[]; as shown below.

C 20

With C 20 pointer/references to array of unknown bound can point/refer to regular arrays(pointer to array of known bound). So with C 20 the following is valid:

class Animal {
  // ...
};

class Continent {
  public:
    Animal (*animalList)[]; // animalList is of type pointer to array of animals
    Continent(Animal (*ptr)[]): animalList(ptr)
    {
        
    }
   
};
int main()
{
    Animal a[5]{};
    Continent c(&a);
    return 0;
}

Demo

You can do the same with references:

class Animal {
  // ...
};

class Continent {
  public:
    Animal (&animalList)[]; // animalList is of type pointer to array of animals
    Continent(Animal (&ptr)[]): animalList(ptr)
    {
        
    }
   
};
int main()
{
    Animal a[5]{};
    Continent c(a);
    return 0;
}

Demo


You can also use std::vector if you want the container to be flexible like we can change its size at runtime. Additionally, using std::array instead of built in array is also an option for avoiding things like array to pointer decay that happens implicitly with built in arrays.

CodePudding user response:

A possible solution can start as follows:

First we need to know the size of the array of pointers of your animal type but we do not want to give the number explicit as an additional parameter. To pass only a pointer to an array is not a solution as we need later on the number of elements in the array! Or we use an additional end mark in the array, maybe a nullptr, but this also waists space and is error prone. And we do not want to create first an array and pass that in a second step into the constructor. So we try to make it more convenient. There is a simply syntax for passing arrays which already contain the size of the array which is:

Animal*(&in)[N]

In this case, N is automatically deduced at compile time to the size of the array! Fine!

In a next step, we want to define the array also directly by passing it. A result will be, that we get a temporary. As the above syntax only allow us a lvalue reference but not a rvalue reference, we have to write:

Animal*(&&in)[N]

Now we can define a class type which has a compile time parameter for the size, which is simply:

template < size_t N> class Continent;

And as C can deduce template parameters from the constructor of that class, we do not need to pass the number of elements, we simply pass the array itself. The compiler deduces the size automatically for us!

Now we have:

template < size_t N> class Continent {
constexpr Continent(Animal*(&&in)[N]) {}
}

OK, that works as expected. But now, as we get only an rvalue reference, we need to copy the elements inside the constructor. That should not take any time. Is it possible? Yes! We can simply copy inside the constructor, there is no need to do it in the initializer list of the constructor as the constructor is defined as constexpr. For simplicity we can use std::copy. And we need some storage for our pointers. An array of known size in compile time is std::array. There is really no need of a std::vector, because it will add more overhead which is, as you want, totally unneeded!

To make it a real working example, I add some test code also. The final result will be:

class Animal {
    public:
    virtual void Print() = 0; 
};

class Lion: public Animal
{
    void Print() override { std::cout << "I am a Lion" << std::endl; }
};

class Tiger: public Animal
{
    void Print() override { std::cout << "I am a Tiger" << std::endl; }
};



template < size_t N>
class Continent {
    public:
        std::array<Animal*, N> arr; 
        constexpr Continent(Animal*(&&in)[N]) : arr{} 
        {
            std::copy(in, in   N, arr.begin());
        }

        void Print()
        {
            for ( auto ptr: arr )
            {
                ptr->Print();
            }
        }
};

int main()
{   
    Tiger t;
    Lion l;
    
    Continent c{ { &t, &l } };
    c.Print();
}

See it working here

If you like or need it, you can also add the constructor for lvalue references.

The solution works from C 17, there is no need of obscure pointer to array of pointers syntax and we can pass directly temporary objects and deduce the size without any need of manual passing the size in the caller side of the code.

A drawback is, that each template instance generates own code. For the constructor a storage there is no overhead, as it is needed anyway, even if we use a std::vector. If we have a huge amount of functionality and the code size becomes bigger, we can move the generic code to a base class and derive it from the size dependent one. That fits best for both requirements!

As a result, we have something like:

class Animal {
    public:
    virtual void Print() = 0; 
};

class Lion: public Animal
{
    void Print() override { std::cout << "I am a Lion" << std::endl; }
};

class Tiger: public Animal
{
    void Print() override { std::cout << "I am a Tiger" << std::endl; }
};

class Continent_Generic
{
    public:
        void Print(Animal* ptr[], size_t n)
        {
            for ( unsigned int cnt = 0; cnt < n; cnt   )
            {
                ptr[cnt]->Print();
            }
        }

};

template < size_t N>
class Continent: public Continent_Generic {
    public:
        std::array<Animal*, N> arr; 
        constexpr Continent(Animal*(&&in)[N]) 
        {
            std::copy(in, in   N, arr.begin());
        }

        void Print()
        {
            Continent_Generic::Print( arr.data(), N);
        }

};

int main()
{
    Tiger t;
    Lion l;

    Continent c{ { &t, &l } }; 
    c.Print();
}

See it working here

  • Related