Сколько инициализации может/должно быть в списках инициализации C++

Это мой первый пост. Я полагаю, что знаю лучшие практики по stackoverflow, но, вероятно, не на 100%. Я считаю, что нет конкретного поста, посвященного моему допросу; также я надеюсь, что это не слишком расплывчато.

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

Включение (всех?) работы init в списки инициализации кажется хорошей идеей по двум причинам, которые приходят мне в голову, а именно:

  • Приобретение ресурсов — это инициализация

    Насколько я знаю, самый простой способ гарантировать правильную инициализацию членов при получении ресурсов — убедиться, что то, что находится внутри скобок списка инициализации, правильно при оценке.

    class A
    {
      public:
        A(const B & b, const C & c)
        : _c(c)
        {
            /* _c was allocated and defined at the same time */
            /* _b is allocated but its content is undefined */
            _b = b;
        }
      private:
        B _b;
        C _c;
    }
    
  • const участников класса

    Использование списков инициализации — единственный правильный способ использования const элементов, которые могут содержать фактический контент.

    class A
    {
      public:
        A(int m, int n, int p)
        : _m(m) /* correct, _m will be initialized to m */
        {
            _n = n; /* incorrect, _n is already initialized to an undefined value */
            *(const_cast<int*>(&_p)) = p; /* technically valid, but ugly and not particularly RAII-friendly */
        }
      private:
        const int _m, _n, _p;
    }
    

Однако некоторые проблемы, похоже, влияют на использование списков инициализации:

  • #P9#
    #P10# #P11#
    #P12#
    A(int in) : _n(in), _m(_n) {}
    
    #P13# #P14# #P15# #P16#
    int in_to_n(int in)
    {
        /* ... */
        return n;
    }
    
    int modify(int n)
    {
        /* ... */
        return modified;
    }
    
    A::A(int in)
    : _n(in_to_n(in))
    , _n_modified(modify(in_to_n(in)))
    {}
    
    #P17#
  • Сколько работ вы можете включить в список?

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

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

    Кроме того, когда вы пытаетесь применить решение к предыдущей проблеме, при инициализации вашего экземпляра вы можете сделать очень мало независимой работы... Это обычно становится большим, если у вас есть длинная последовательность атрибутов для инициализации с внутренними зависимостями. Это начинает выглядеть как просто программа, переведенная в список инициализации; Я предполагаю, что это не то, к чему должен переходить С++?

  • Множественные инициализации

    Часто вычисляют две переменные одновременно. Установка двух переменных одновременно в списке инициализации означает:

    • использование уродливого промежуточного атрибута

      struct InnerAData
      {
          B b;
          C c;
      };
      /* must be exported with the class definition (ugly) */
      
      class A
      {
        public:
          A(const D & input)
          : _inner(work(input))
          , _b(_inner.b)
          , _c(_inner.c) {}
        private:
          B _b;
          C _c;
          InnerAData _inner;
      }
      

      Это ужасно и приводит к дополнительным бесполезным копиям.

    • или какой-то уродливый хак

       class A
      {
        public:
          A(const D & input) : _b(work(input)) {}
        private:
          B _b;
          C _c;
          B work(const D & input)
          {
              /* ... work ... */
              _c = ...;
          }
      }
      

      Это еще более ужасно и даже не работает с const или не встроенными атрибутами типа.

  • хранить вещи const

    Иногда может потребоваться большая часть ctor, чтобы выяснить значение, которое нужно присвоить атрибуту, поэтому проверка того, что это const, и, следовательно, перемещение работы в список инициализации, может показаться ограниченным. Я не буду приводить полный пример, но думаю что-то вроде вычисления данных из имени файла по умолчанию, затем вычисления полного имени файла из этих данных, затем проверки существования соответствующего файла для установки константного логического значения и т. д.

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


Итак, вот сложная часть: задать конкретный вопрос! Сталкивались ли вы с подобными проблемами, нашли ли вы лучшее решение, если нет, какой урок следует усвоить или я что-то упустил?

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


Кроме того, я действительно не уверен, почему значения инициализируются в этом порядке, а не в порядке списка. Мне устно сказали, что это потому, что атрибуты в стеке упорядочены, и компилятор должен гарантировать, что данные стека никогда не превышают SP. Я не уверен, как это окончательный ответ ... почти уверен, что можно реализовать безопасные произвольно переупорядоченные списки инициализации, поправьте меня, если я ошибаюсь.


person John Gliksberg    schedule 06.07.2016    source источник
comment
порядок — хорошие компиляторы могут вас об этом предупредить.   -  person Karoly Horvath    schedule 06.07.2016
comment
@KarolyHorvath true, если вы можете полагаться на предупреждения компилятора, которые действительно помогают. Я оказался в контексте, где это не так, и я думаю, это подталкивает меня к мысли, что необходимость соблюдать порядок объявления несовершенна. Но я согласен с вами.   -  person John Gliksberg    schedule 06.07.2016
comment
Порядок инициализации имеет значение: если у вас есть член, который зависит от других членов, вам нужно поместить член в правильном порядке, чтобы он работал правильно.   -  person Mine    schedule 06.07.2016
comment
@Mine Я знаю об этом, я просто говорю, что не понимаю, почему это было разработано таким образом, и я добавляю, что нахожусь в ситуации, когда это останавливает использование списков инициализации, как только есть внутренние зависимости.   -  person John Gliksberg    schedule 06.07.2016
comment
Не забывайте: как и const, ссылки-члены (Class &c;) тоже должны быть в списках инициализации.   -  person John Burger    schedule 06.07.2016
comment
Причина, по которой конструктор инициализирует элементы в том порядке, в котором они появляются в заголовочном файле (а не в конструкторе), заключается в том, что существует только один deструктор, и он должен уничтожить элементы в известный порядок. Какой порядок? Существует только один окончательный, неизменный порядок, и он указан в определении класса. Если бы разные конструкторы могли инициализировать элементы в произвольном порядке, деструктор должен был бы выяснить, каков этот порядок, чтобы гарантировать, что он уничтожит их в обратном порядке. Блекч!   -  person John Burger    schedule 06.07.2016
comment
@JohnBurger спасибо за оба ваших объяснения !! Особенно о порядке, который, я думаю, действительно останавливает любую дискуссию.   -  person John Gliksberg    schedule 06.07.2016
comment
@JohnBurger Если бы разные конструкторы могли инициализировать элементы в произвольном порядке, деструктор должен был бы выяснить, каков был этот порядок, чтобы гарантировать, что он уничтожит их в обратном порядке Только если вы настаиваете на уничтожении объектов в обратный порядок, ненужное требование.   -  person curiousguy    schedule 22.07.2016
comment
*(const_cast<int*>(&_p)) = p; /* technically valid, нет!   -  person curiousguy    schedule 22.07.2016
comment
@curiousguy Только если вы настаиваете... ...ненужное требование. Я настаиваю. Мне нужно знать, что объект, который я передал во время строительства, все еще будет там во время разрушения. Если компилятор может изменить порядок переменных класса за моей спиной, то я больше ничего не могу гарантировать!   -  person John Burger    schedule 22.07.2016


Ответы (2)


В вашем коде:

class A
{
  public:
    A(const B & b, const C & c)
    : _c(c)
    {
        /* _c was allocated and defined at the same time */
        /* _b is allocated but its content is undefined */
        _b = b;
    }
  private:
    B _b;
    C _c;
}

конструктор вызывает B::B(), а затем B::operator=, что может быть проблемой, если какой-либо из них не существует, является дорогостоящим или не реализован правильно в соответствии с рекомендациями RAII и правила трех. Эмпирическое правило состоит в том, чтобы всегда предпочитать список инициализаторов, если это возможно.

person majk    schedule 06.07.2016
comment
Да, в этом смысл моего примера, я просто пытался показать две альтернативы. Спасибо за ответ и извините, если я был не ясен! - person John Gliksberg; 06.07.2016

Начиная с С++ 11, альтернативой является использование делегирующих конструкторов:

struct InnerData;
InnerData (work(const D&);

class A
{
  public:
    A(const D & input) : A(work(input)) {}

  private:
    A(const InnerAData&);
  private:
    const B _b;
    const C _c;
};

И (это может быть встроено, но затем видно в заголовке)

struct InnerAData
{
    B b;
    C c;
};

A::A(const InnerAData& inner) : _b(inner.b), _c(inner.c) {}
person Jarod42    schedule 06.07.2016
comment
Спасибо за альтернативу. Я не вижу, как это принципиально что-то меняет для пользователя (список инициализации может быть скрыт в .cpp независимо), и ему по-прежнему необходимо экспортировать определение InnerData вместе с классом. - person John Gliksberg; 06.07.2016
comment
Кстати, если InnerData является просто копией какой-то внутренней части A, имеет смысл удалить эту копию и напрямую использовать InnerData. - person Jarod42; 06.07.2016