Список инициализаторов конструктора против дорогостоящих операций

#c #constructor

#c #конструктор

Вопрос:

Рассмотрим гипотетический сценарий, в котором два класса могут быть сконструированы по умолчанию или друг из друга, но любой способ считается дорогостоящим (далее следует намеренно надуманный пример):

 struct PrivateKey;

struct PublicKey {
  PublicKey(); // generate a random public key (1 minute)
  PublicKey(const PrivateKeyamp; 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 PublicKeyamp; a); // find a private key corresponding to a public key (1 year)
  ...members...
};
  

(Это, конечно, можно было бы свести к одному классу, но на обоснованность вопроса это не влияет. Допустим, ради согласованности нет симметрии между одним и другим.)

Теперь есть структура, которая содержит экземпляры обоих и нуждается в этой перекрестной инициализации. Однако нам могут понадобиться оба направления, поэтому списки инициализаторов на самом деле не могут его сократить (они выполняются не в указанном порядке, а в порядке определения элементов, и здесь порядок не может быть исправлен):

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

Конечно, я мог бы попробовать:

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

но у этого есть несколько проблем, из которых запуск дорогостоящей конструкции по умолчанию PublicKey во втором конструкторе X , чтобы сразу же выдать результат, является только первой. Могут быть побочные эффекты для PublicKey::PublicKey() . И то, и другое все еще можно было бы смягчить, создав дешевый частный конструктор, доступный только другу, X который поместил бы класс в некоторое фиктивное состояние, но добавил бы некоторые ссылочные или постоянные члены, и класс не может быть переназначаемым или заменяемым, запрещая любые изменения в теле X::X(float) . Есть ли лучший шаблон для подражания?

Комментарии:

1. Используйте PublicKey *a и PrivateKey *b и создавайте их внутри тела конструктора { ... } .

2. @AJNeufeld Поле для ответа находится внизу.

3. Кристально чистый. Боже, о чем я вообще думал? Хотите опубликовать ответ?

4. Кстати, потеря 1 минуты за 1 год на самом деле не проблема.

5. @Jarod42 понял это после публикации. Но я не хотел думать о другой проблеме, которая обладает аналогичными свойствами.

Ответ №1:

Проблемы с порядком построения можно избежать, используя указатели на содержащиеся классы вместо непосредственного встраивания содержащихся классов и самостоятельного построения содержащихся объектов внутри тела конструктора.

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

Комментарии:

1. Возможно, даже если код не требует пояснений, в коде должно быть объяснение, потому что это может помочь тем, кто в будущем.

2. Вы могли бы упомянуть, что на практике std::unique_ptr следует использовать вместо необработанных указателей.

3. Теперь, когда вы используете, unique_ptr вы должны использовать make_unique в телах конструктора.

4. @NathanOliver Из-за множества предложенных улучшений я изменил это на вики сообщества. Дополнительные улучшения могут быть внесены непосредственно в ответ.

5. Сделал некоторые сам, чтобы спасти работу других. Большое спасибо!

Ответ №2:

Если классы, по крайней мере, могут быть сконструированы для перемещения, вы должны быть в состоянии сделать это:

 struct KeyPair
{
  PublicKey a;
  PrivateKey b;
  KeyPair(std::pair<PublicKey, PrivateKey> amp;amp;data) :
    a(std::move(data.first)),
    b(std::move(data.second))
  {}
};

std::pair<PublicKey, PrivateKey> computePublicFirst()
{
  PublicKey a;
  PrivateKey b(a);
  return {std::move(a), std::move(b)};
}

std::pair<PublicKey, PrivateKey> computePrivateFirst()
{
  PrivateKey b;
  PublicKey a(b);
  return {std::move(a), std::move(b)};
}

struct X
{
  KeyPair keys;
  X(int) : keys(computePublicFirst()) {}
  X(float) : keys(computePrivateFirst()) {}
};
  

Никаких назначений перемещения не происходит, только перемещение конструкции.

Комментарии:

1. Я хотел бы принять оба решения. Я выбрал другой ответ за его элегантность и семантическую ясность, но ваш идеально подходит, если вы хотите устранить дополнительное разрешение указателя при каждом вызове метода A или B, что я часто считаю важным.