Представляет значение, которое не может быть инициализировано ctor-initialization-list

Я пишу класс C с членом foo типа foo_t, Этот член должен быть определен и действителен в течение всей жизни C пример; Тем не менее, у меня нет необходимой информации, чтобы построить его во время компиляции, то есть я не могу использовать

class C {
    foo_t foo { arg };
}

И при этом я не могу построить это, когда C вызывается, то есть я не могу иметь

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

скорее я могу построить его только после некоторого кода в C Ктор побежал. Изменить: Причиной этого может быть то, что мне нужно запустить некоторый код с побочными эффектами (например, дисковый или сетевой ввод / вывод), чтобы получить аргумент конструкции; и мне нужно, чтобы этот код выполнялся также для возможности инициализации других членов, поэтому я не могу просто вызвать его несколько раз как свободную функцию из списка инициализации.

Итак, как я должен представлять foo?

  • Если foo_t может быть сконструирован по умолчанию с некоторым фиктивным / недействительным / пустым / нулевым значением, тогда я могу позволить этому произойти и быть уверенным в том, что к нему никогда не будет обращаться в этом фиктивном состоянии. Ущерб: декларация foo в C не означает, что это всегда верно.
  • Если foo_t только имеет действительное состояние, то есть я не могу создать его вообще, пока не получу соответствующую информацию, тогда:

    • я могу использовать std::unique_ptr<foo_t>; изначально это будет nullptr, а затем будет назначен. Ущерб: нет признаков того, что он никогда не будет нулевым после C() заключает; Бесполезное распределение.
    • я могу использовать std::optional<foo_t>; Изначально это будет nullopt, а затем будет назначен. Ущерб: нет признаков того, что он никогда не будет пустым после C() заключает; требует C++14; слово "необязательно" предполагает, что это "необязательно" иметь foo в то время как это не так.

Меня больше интересует второй случай, так как в первом случае неоднозначность foo_t Действительность является своего рода встроенной. Есть ли лучшая альтернатива двум, о которых я говорил?

Примечание: мы не можем изменить foo_t,

2 ответа

В общем, если вы можете построить foo_t в телах конструктора некоторого класса (без списков инициализаторов членов) вы можете изменить свой код так, чтобы ваш класс теперь имел foo_t Атрибут и его конструкторы либо делегируют конструкцию, либо конструируют ее внутри своих списков инициализаторов.

По сути, в большинстве случаев вы можете переписать ваш проблемный конструктор так, чтобы он делегировал другому конструктору, предоставляя ему необходимую информацию для создания foo_t экземпляр в списке инициализатора члена (который я быстро и неофициально проиллюстрировал в комментариях следующим "примером" https://ideone.com/ubbbb7)


В более общем смысле, и если по какой-либо причине конструкция кортежа окажется проблемой, последующее преобразование будет (в общем) работать. По общему признанию, это немного долго (и некрасиво), но имейте в виду, что это ради общности, и что, возможно, можно упростить вещи на практике.

Давайте предположим, что у нас есть конструктор, где мы создаем foo_tдля простоты мы будем далее предполагать, что он имеет следующую форму:

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

Где вызовы функции возможно видоизменяют аргументы. Мы можем делегировать один раз для устранения декларации local_1 в теле конструктора, а затем еще раз, чтобы избавиться от вызова 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);
}

живой пример

Очевидно, что f можно сделать фактическим членом C и создать его в списке инициализации этого последнего конструктора.

Можно обобщить любое количество локальных переменных (и аргументов). Однако я предположил, что у нашего начального конструктора не было списка инициализаторов членов. Если бы он был, нам, возможно, понадобилось бы:

  • скопировать некоторые из начальных arg_iперед тем как они были видоизменены и передают копии по цепочке конструктора, чтобы в конечном итоге их можно было использовать для конструирования других членов в списке инициализатора члена
  • предварительно создать экземпляры элементов и передать их по цепочке конструктора, чтобы в конечном итоге их можно было использовать для перемещения-создания фактических элементов в списке инициализаторов элементов

Последний должен быть выбран, если по какой-то причине конструктор члена будет иметь побочные эффекты.


Однако есть случай, когда все это разваливается. Давайте рассмотрим следующий сценарий:

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

В этой ситуации, по общему признанию, я не знаю, как переписать этот класс, но я считаю, что это достаточно редко, чтобы не иметь большого значения.

Давайте рассмотрим немного более конкретный случай

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

    static whatever_t read_from_file();
}

и давайте предположим, что нежелательно считывать одни и те же данные из файла дважды.

Одним из возможных подходов может быть:

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

Спасибо @VittorioRomeo за предложения по его улучшению.

Wandbox

Другие вопросы по тегам