1

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;
}

Godbolt

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;
};
nowhere_
  • 23
  • 4
  • I would not use std::span as a member variable, since span is a non owning view. And IMO is best created when needed and then passed to functions that can use it for the duration of the call. The array while unitialized has a well defined place in memory (only its content is not initialize) so the span can be created. – Pepijn Kramer Jul 05 '23 at 09:26
  • Thanks, this has given me a suitable thought-provoking impulse. I realized that I could use a [template method](https://godbolt.org/z/411qWEshb) instead, which does not suffer from the original drawbacks. I will keep the original code in the question, because I am still interested in the specifics of such a case. – nowhere_ Jul 05 '23 at 09:51
  • 1
    It would appear that, since `std::array` is trivially constructible, its lifetime starts as soon as storage is allocated, per [\[basic.life\]/1](https://timsong-cpp.github.io/cppwp/n4140/basic.life#1). By the time `Generic{this->arr}` runs, it's already OK to call `this->arr.data()` (which is what `span` constructor does). – Igor Tandetnik Jul 07 '23 at 15:42

1 Answers1

0

Reading [basic.life] as suggested by @Igor Tandetnik gave me an idea on how to approach the questions. I am posting my findings here for future reference:

Does Generic{this->arr} violate the standard/cause undefined behavior?

No, because arr is alive at that point although not being initialized yet. The lifetime of objects with trivial initialization starts when their storage is allocated:

(3.8.1)

The lifetime of an object of type T begins when:

  • storage with the proper alignment and size for type T is obtained, and
  • if the object has non-trivial initialization, its initialization is complete.

std::array<int, capacity> is trivially constructable, because int is trivially constructable (i.e. int meets the requirements of [class.ctor]). Further, the internal C-style array is subject to:

(3.8.2)

The lifetime of an array object starts as soon as storage with proper size and alignment is obtained.

Note: This is not necessarily true for every template instantiation:

#include <type_traits>
#include <array>
#include <iostream>
#include <initializer_list>

struct NotTrivial
{
    NotTrivial() :
        nt{3}
    {
    }

    int nt;
};

using Test1 = std::array<NotTrivial, 2>;
using Test2 = std::array<int, 2>;

int main()
{
    bool t1 = std::is_trivially_constructible<Test1>::value;
    bool t2 = std::is_trivially_constructible<Test2>::value;
    std::cout << t1 << std::endl; // 0/false
    std::cout << t2 << std::endl; // 1/true
}

In the provided example the variables have automatic storage duration, so the lifetime of their std::array members starts when entering main. As a result, the call of data() is well defined.

Are there cases where calling a method on an uninitialized object is tolerable?

It is a matter of lifetime, rather than initialization. There are exceptions for when storage has been allocated but the liftime has not started yet. In these cases the pointers or glvalues refer to allocated storage.

(3.8.5)

[...] using the pointer as if the pointer were of type void*, is well-defined.

(3.8.6)

[...] using the properties of the glvalue that do not depend on its value is well-defined.

In both cases calling a non-static member method is not one of those exceptions. Instead, it is explicitly listed to cause undefined behavior.

In the case of trivially constructable types there can be a "delay" between the start of lifetime and their initialization. Calling non-static member functions is well defined during this delay, but may not produce the desired result.

Section(s) in the standard where the determination of member addresses during construction is regulated

After (re-)reading [intro.memory], I think the standard is deliberately vague in this area. For all the standard knows one could implement a C++ compiler for a futuristic machine, featuring a previously unknown type of memory addressing scheme. Therefore, the determination of member addresses during construction is implementation defined.

nowhere_
  • 23
  • 4