3

In C++, any member of a class which is not constructed in a member initialization list is default constructed before the containing class's constructor is executed. However, this appears to be very wasteful if that member variable is just going to get constructed anyway inside the constructor of the class it resides in.

I've provided an example below to clarify what I mean. Here, the Example class has a member variable x of type LargeIntimidatingClass. Using the member initialization list (the first constructor in Example) x is only constructed once. However, if x cannot be reasonably constructed using the member initialization list, it gets constructed twice!

//This class used as part of the example class further below
class LargeIntimidatingClass {
    // ...
    //many member variables and functions
    // ...

    LargeIntimidatingClass() {
        //Painfully expensive default initializer
    }

    LargeIntimidatingClass(int a, double b) {
        //Complicated calculations involving a and b
    }
};

//Here, this class has a LargeIntimidatingClass as a member variable.
class Example {
    LargeIntimidatingClass x;
    char c;

    //Basic member initialization list constructor. Efficient!
    Example(int a, double b, char c) : x(a,b), c(c) {}

    //What if the parameters to the LargeIntimidatingClass's constructor
    //need to be computed inside the Example's constructor beforehand?
    Example(std::string sophisticatedArgument) {
        //Oh no! x has already been default initialized (unnecessarily!)

        int a = something1(sophisticatedArgument);
        double b = something2(sophisticatedArgument);
        //x gets constructed again! Previous (default) x is totally wasted!
        x = LargeIntimidatingClass(a,b);

        c = something3(sophisticatedArgument);
    }
};

Yes, I realize that in this silly example you could write Example(string s) : x(f1(s),f2(s)), c(f3(s)) {}, but I'm sure you can imagine a situation where shoving a bunch of logic into a member initialization list is cumbersome (or even impossible).

Is it possible to disable a member variable's default constructor when it is not listed in the member initialization list?

Marco Merlini
  • 875
  • 7
  • 29

4 Answers4

3

You cannot disable the construction. All class members must be initialized before the body of the constructor is reached. That said, you can easily work around the problem. You can add a private static member function that gets a and b and returns a LargeIntimidatingClass from it like

class Example {
    LargeIntimidatingClass x;
    char c;
    static LargeIntimidatingClass make_LargeIntimidatingClass(std::string sophisticatedArgument)
    {
        int a = something1(sophisticatedArgument);
        double b = something2(sophisticatedArgument);
        return LargeIntimidatingClass(a,b);
    }
    static char make_c(std::string sophisticatedArgument)
    {
        return something3(sophisticatedArgument);
    }
public:

    //Basic member initialization list constructor. Efficient!
    Example(int a, double b, char c) : x(a,b), c(c) {}

    // now we use helpers to initialize in the member initialization list
    Example(std::string sophisticatedArgument) : x(make_LargeIntimidatingClass(sophisticatedArgument), c(make_c(sophisticatedArgument) {
        //now everything is initialized correctly
    }
};
NathanOliver
  • 171,901
  • 28
  • 288
  • 402
0

Disable an integral part of how the language works? No. But you can either refactor to work with the language, or get around it in various ways.

  1. Have a (smart) pointer member to the expansive class.
  2. Have the member be std:aligned_storage and create the object by placement new. Then manage the lifetime of the object carefully yourself.
  3. Hold a std:optional. Manage the initialization and let the library type manage the rest for a little overhead in object size.
StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
0

Generally std::optional should be used because it is more safe and expressive. In performance critical code, you can use the following method:

Automatic construction and destruction of values is disabled for unions. Thus, wrap an anonymous (or named) union, and handle construction/destruction manually. Again, this has to be carefully controlled to avoid undefined behavior. Also note that the wrapper class can only propagate trivial destruction, but not trivial default construction.

Note also that movement, copying, and assignment must be manually handled. If you know that the value will always be initialized before use, you can wrap another helper around Uninitialized below to automatically handle destruction, and assignment.

Following, is a complete helper class I wrote for this situation for C++20. Of course all behavior can be rewritten for older versions of C++.


template <class T>
class Uninitialized {
private:
    // This is the solution!
    union {
        [[no_unique_address]] T _value;
    };


public:
    using value_type = T;

    // Won't construct the member.
    constexpr Uninitialized() noexcept {}

    // Constructs in place.
    template <class... Args>
        requires std::constructible_from<T, Args...>
    constexpr Uninitialized(std::in_place_t, Args &&...args) //
        noexcept(std::is_nothrow_constructible_v<T, Args...>)
        : _value(std::forward<Args>(args)...)
    {}

    // Constructs in place, and allows initializer_list inference. 
    // See std::optional for a similar pattern.
    template <class U, class... Args>
        requires std::constructible_from<T, Args...>
    constexpr Uninitialized(
        std::in_place_t, std::initializer_list<U> ilist, Args &&...args) //
        noexcept(std::is_nothrow_constructible_v<
                 T, std::initializer_list<U> &, Args...>)
        : _value(ilist, std::forward<Args>(args)...)
    {}

    // Constrained overload (with precedence) for trivially destructible values.
    constexpr ~Uninitialized() requires(std::is_trivially_destructible_v<T>) =
        default;

    // Does not call destructor, but is not trivial.
    constexpr ~Uninitialized() {}
    

    constexpr T *operator->() noexcept { return std::addressof(_value); }

    constexpr const T *operator->() const noexcept
    {
        return std::addressof(_value);
    }

    // Accessors, with complete overload qualification set.
    constexpr T &operator*() &noexcept { return _value; }

    constexpr T &&operator*() &&noexcept { return std::move(_value); }

    constexpr const T &operator*() const &noexcept { return _value; }

    constexpr const T &&operator*() const &&noexcept
    {
        return std::move(_value);
    }

    constexpr T &value() &noexcept { return _value; }

    constexpr T &&value() &&noexcept { return std::move(_value); }

    constexpr const T &value() const &noexcept { return _value; }

    constexpr const T &&value() const &&noexcept { return std::move(_value); }

    template <class... Args>
        requires(std::constructible_from<T, Args...>)
    constexpr T &construct(Args &&...args) //
        noexcept(std::is_nothrow_constructible_v<T, Args...>)
    {
        std::ranges::construct_at(
            std::addressof(_value), std::forward<Args>(args)...);
        return _value;
    }

    template <class U, class... Args>
        requires(
            std::constructible_from<T, std::initializer_list<U> &, Args...>)
    constexpr T &construct(std::initialize_list<U> ilist, Args &&...args) //
        noexcept(std::is_nothrow_constructible_v<
                 T, std::initializer_list<U> &, Args...>)
    {
        std::ranges::construct_at(
            std::addressof(_value), std::forward<Args>(args)...);
        return _value;
    }

    constexpr void destroy() noexcept(std::is_nothrow_destructible_v<T>)
    {
        if constexpr (!std::is_trivially_destructible_v<T>) {
            std::ranges::destroy_at(std::addressof(_value));
        }
    }
};
Hunter Kohler
  • 1,885
  • 1
  • 18
  • 23
-1

Is it possible to disable a member variable's default constructor when it is not listed in the member initialization list?

No, that's not possible.

πάντα ῥεῖ
  • 1
  • 13
  • 116
  • 190
Jesper Juhl
  • 30,449
  • 3
  • 47
  • 70