简体   繁体   中英

Representing a value which can't be ctor-initialization-list initialized

I'm writing a class C with a member foo of type foo_t . This member must be defined and valid throughout the liftime of a C instance; however, I don't have the necessary information to construct it at compile time, ie I can't use

class C {
    foo_t foo { arg };
}

Nor can I construct it when the ctor of C gets invoked, ie I can't have

class C {
    foo_t foo;
    C (whatever) : foo(compute_arg(whatever)) { }
}

rather, I am only able to construct it after some code within the C ctor has run. Edit: The reason for this may be that I need to run some code with side-effects (eg disk or network I/O) to obtain the construction argument; and I need that code to run also to be able to initialize other members, so I can't just call it several times as a free function from the initialization list.

So, how should I represent foo ?

  • If foo_t can be default-constructed with some dummy/invalid/empty/null value, then I can let that happen, and be safe in the knowledge that it will never be accessed in that dummy state. Detriment: The declaration of foo in C does not indicate that it's always valid .
  • If foo_t only has a valid state, ie I can't construct it at all until I have the relevant information, then:

    • I can use std::unique_ptr<foo_t> ; initially it will be nullptr , then be assigned to. Detriments: No indication that it'll never be null after C() concludes; A useless allocation.
    • I can use std::optional<foo_t> ; Initially it will be nullopt , then be assigned to. Detriment: No indication that it'll never be empty after C() concludes; requires C++14; the word "optional" suggests that it's "optional" to have foo , while it isn't .

I'm more interested in the second case, since in the first case the ambiguity about a foo_t 's validity is kind of built-in. Is there a better alternative to the two I mentioned?

Note: We can't alter foo_t .

Let's consider a bit more specific case

struct C {
    foo_t foo1;
    foo_t foo2;
    C () : 
        foo1(read_from_file()),
        foo2(read_from_file()),
    { }

    static whatever_t read_from_file();
}

and let's assume it's not desired to read same data from the file twice.

One possible approach can be:

struct C {
    foo_t foo1;
    foo_t foo2;

    C(): C{Create()} {}

private:
    static C Create()
    {
        return C{read_from_file()};
    }

    C(whatever_t whatever):
        foo1{whatever},
        foo2{whatever}
    {}

    static whatever_t read_from_file();
}

Thanks to @VittorioRomeo for suggestions to improve it.

Wandbox

In general if you can construct a foo_t in the constructor bodies of some class (without member initializer lists), then, you can modify your code so that your class now has a foo_t attribute and its constructors either delegate construction or construct it inside their member initializer lists.

Basically, in most cases, you can rewrite your problematic constructor so that it delegates to another constructor while providing it with necessary information to construct a foo_t instance in the member initializer list (which I quickly and informally illustrated in the comments with the following "example" https://ideone.com/ubbbb7 )


More generally, and if the tuple construction would happen to be a problem for some reason, the following transformation will (in general) work. It's admittedly a bit long (and ugly), but bear in mind it's for generality sake and that one could probably simplify things in practice.

Let's assume we have a constructor where we construct a foo_t , for the sake of simplicity, we'll further assume it to be of the following form :

C::C(T1 arg_1, T2 arg_2) {
    side_effects(arg_1, arg_2);
    TL1 local(arg_1, arg_2);
    second_side_effects(arg_1, arg_2, local);
    foo_t f(arg_1, arg_2, local); // the actual construction
    final_side_effects(arg_1, arg_2, local, f);
}

Where the function calls possibly mutate the arguments. We can delegate once to eliminate the declaration of local_1 in the constructor body, then once again to get rid of the call to second_side_effects(arg_1, arg_2, local) .

C::C(T1 arg_1, T2 arg_2)
: C::C(arg_1, arg_2
      ,([](T1& a, T2& b){
          side_effects(a, b);
        }(arg_1, arg_2), TL1(a, b))) {}

C::C(T1& arg_1, T2& arg_2, TL1&& local)
: C::C(arg_1, arg_2
      ,[](T1& a, T2& b, TL1& c) -> TL1& {
          second_side_effects(a, b, c);
          return c;
      }(arg_1, arg_2, local)) {}

C::C(T1& arg_1, T2& arg_2, TL1& local) {
    foo_t f(arg_1, arg_2, local); // the actual construction
    final_side_effects(arg_1, arg_2, local, f);
}

live example

Clearly, f could be made an actual member of C and be constructed in the member initialization list of that last constructor.

One could generalize for any number of local variables (and arguments). I however assumed that our initial constructor didn't have any member initializer list. If it had one, we may have needed to either:

  • copy some of the initial arg_i 's before they were mutated and pass the copies along the constructor chain so that they could ultimately be used to construct the other members in the member initializer list
  • preconstruct instances of the members and pass them along the constructor chain so that they could ultimately be used to move-construct the actual members in the member initializer list

The latter must be chosen if for some reason, the constructor of a member would have side effects.


There is however a case where this all falls apart. Let's consider the following scenario:

#include <memory>

struct state_t; // non copyable, non movable

// irreversible function that mutates an instance of state_t
state_t& next_state(state_t&);

struct foo_t {
    foo_t() = delete;
    foo_t(const foo_t&) = delete;
    foo_t(const state_t&);
};

// definitions are elsewhere

class C {
public:
    struct x_first_tag {};
    struct y_first_tag {};

    // this constructor prevents us from reordering x and y
    C(state_t& s, x_first_tag = {})
    : x(new foo_t(s))
    , y(next_state(s)) {}

    // if x and y were both constructed in the member initializer list
    // x would be constructed before y
    // but the construction of y requires the original s which will
    // be definitively lost when we're ready to construct x !
    C(state_t& s, y_first_tag = {})
    : x(nullptr)
    , y(s) {
        next_state(s);
        x.reset(new foo_t(s));
    }

private:
    std::unique_ptr<foo_t> x; // can we make that a foo_t ?
    foo_t y;
};

In that situation, I admittedly have no idea how to rewrite this class, but I deem it rare enough to not really matter.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM