The Vee The Vee - 1 month ago 9
C++ Question

Constructor initializer list vs. expensive operations

Consider a hypothetical scenario where two classes can be default-constructed or constructed from each other but either way is considered to be expensive (intentionally contrived example follows):

struct PrivateKey;

struct PublicKey {
PublicKey(); // generate a random public key (1 minute)
PublicKey(const PrivateKey& b); // find a public key corresponding to a private key (1 year)
...members...
};

struct PrivateKey {
PrivateKey(); // generate a random private key (1 minute)
PrivateKey(const PublicKey& a); // find a private key corresponding to a public key (1 year)
...members...
};


(This could of course be condensed to one class but the validity of the question is unaffected. Let's say for the sake of coherence there's no symmetry between one and the other.)

Now there's a structure which holds instances of both and needs this cross-initialization. However, we may need both directions, so initializer lists can't really cut it (they aren't run in the order listed but in the order the members are defined, and no order can be fixed here):

struct X {
PublicKey a;
PrivateKey b;
X(int): a(), b(a) { }
X(float): b(), a(b) { } // UB: a(b) happens before b is initialized
};


Of course I could try:

struct X {
PublicKey a;
PrivateKey b;
X(int): a(), b(a) { }
X(float): a(), b() { a = PublicKey(b); }
};


but this has multiple issues, of which running the expensive default-construction of
PublicKey
in the second constructor of
X
just to throw the result right away is just the first. There may be side effects to
PublicKey::PublicKey()
. Both could still be mitigated by creating a cheap private constructor exposed only to a friend
X
which would put the class in some dummy state, but toss in some reference or constant members and the class may not be move-assignable or swappable, forbidding any variation on the
X::X(float)
's body. Is there a better pattern to follow?

Answer

The construction order issue can be avoided by using pointers to the contained classes, instead of embedding the contained classes directly, and constructing the contained objects yourself inside the body of the constructor.

struct X {
  std::unique_ptr<PublicKey> a;
  std::unique_ptr<PrivateKey> b;
  X(int) {
    a = std::make_unique<PublicKey>();
    b = std::make_unique<PrivateKey>(*a);
  }
  X(float) {
    b = std::make_unique<PrivateKey>();
    a = std::make_unique<PublicKey>(*b);
  }
};