Being mandated to not use dynamic memory allocation, I came up with the following design:
#include <array>
#include <cstdint>
#include <iostream>
#include <span>
class Generic
{
public:
template <size_t capacity>
class Model;
int Sum() const
{
int result = 0;
for (const int num : numbers)
{
result += num;
}
return result;
}
protected:
Generic(const std::span<int> numbers) :
numbers{numbers}
{
// Do not interact with references to derived
// members, because they are not initialized yet!
}
private:
std::span<int> numbers;
};
template <size_t capacity>
class Generic::Model : public Generic
{
public:
Model(const std::array<int, capacity>& arr) :
Generic{this->arr}, // Does calling the converting constructor cause UB?
arr{arr}
{
}
Model(const Model& other) :
Generic{other}, // Copy state of Generic if any
arr{other.arr}
{
// Link base span with internal array
Generic::numbers = arr;
}
private:
std::array<int, capacity> arr;
};
int main()
{
const Generic::Model<5> test1{{0, 1, 2, 3, 4}};
Generic::Model<3> test2{{0, 1, 2}};
Generic::Model<5> test3{test1};
std::cout << test1.Sum() << std::endl;
std::cout << test2.Sum() << std::endl;
std::cout << test3.Sum() << std::endl;
}
The reasoning behind it is to provide the ownership semantics one would expect, instead of allocating the array outside of the Generic
instance. To minimize template code bloat, the design is split into a Generic
implementation class and a nested Model
class. This limits the code that is dependent on the template parameter.
The remaining question is whether this split is within the limits of the standard. According to this answer passing references or pointers of derived members to the base class constructor is fine, because their addresses are known at that time. While this makes sense to me, I was not able to locate this guarantee in the standard (I searched class.ctor and class.init).
Further, while passing a std::span
can be thought of as a "reference" to an array, it requires to call a converting constructor (e.g. libcxx). This leads to taking the uninitialized array as an argument and calling std::span<T,Extent>::data()
on it. Applying the reasoning from pptaszni this should be fine, because the address of the internal array is known at that time too.
While the minimal example works fine in Godbolt, I experienced some issues with (more complex) applications of this "pattern" on the target hardware, which I sadly cannot reproduce online. The most notable symptom is that in the resulting memory layout the arrays of the derived class overlap with members of the base class. This caused me to doubt the solution. I already tried to replace std::array
with C-style arrays to no avail.
tl;dr
- Does
Generic{this->arr}
violate the standard/cause undefined behavior? - Can you point me to the section(s) in the standard where the determination of member addresses during construction is regulated? (So I can read up on it myself)
- Are there cases where calling a method on an uninitialized object is tolerable?
EDIT:
A less flawed alternative implementation could use a template method, but I am still interested in the specifics of the original implementation.
class Generic
{
public:
template <size_t capacity>
class Model;
int Sum() const
{
int result = 0;
for (const int num : GetNums())
{
result += num;
}
return result;
}
private:
virtual std::span<const int> GetNums() const = 0;
};
template <size_t capacity>
class Generic::Model : public Generic
{
public:
Model(const std::array<int, capacity>& arr) :
arr{arr}
{
}
private:
std::span<const int> GetNums() const override
{
return arr;
}
std::array<int, capacity> arr;
};