Являются ли делегированные C++11 ctors хуже, чем C++03 ctors, вызывающие функции init?

[этот вопрос был сильно отредактирован; простите, я переместил изменения в ответ ниже]

Из Википедии (включая статью) на C++11:

Эта [ новая функция делегирующих конструкторов ] поставляется с оговоркой: C++03 считает, что объект должен быть построен, когда его конструктор завершает выполнение, но C++ 11 считает объект, созданный, когда любой конструктор заканчивает выполнение. Поскольку разрешено выполнение нескольких конструкторов, это будет означать, что каждый делегирующий конструктор будет выполняться для полностью сконструированного объекта своего собственного типа. Конструкторы производных классов будут выполняться после завершения делегирования в их базовых классах ".

Означает ли это, что цепочки делегирования создают уникальный временный объект для каждой ссылки в цепочке делегирования ctor? Такие издержки просто для того, чтобы избежать простого определения функции инициализации, не будут стоить дополнительных издержек.

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

3 ответа

Нет, они эквивалентны. Делегирующий конструктор ведет себя как обычная функция-член, действующая на объекте, созданном предыдущим конструктором.

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

В Разделе 4.3 - Изменения к §15 предлагаемое изменение к стандартным состояниям:

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

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

Цепные делегирующие конструкторы в C++11 несут больше накладных расходов, чем стиль функции инициализации C++03!

См. Проект стандарта C++11 N3242, раздел 15.2. Исключение может возникнуть в блоке выполнения любой ссылки в цепочке делегирования, и C++11 расширяет существующее поведение обработки исключений, чтобы учесть это.

[текст] и акцент мой.

У объекта любой продолжительности хранения, инициализация или уничтожение которого завершается исключением, будут деструкторы, выполненные для всех его полностью построенных подобъектов..., то есть для подобъектов, для которых главный конструктор (12.6.2) завершил выполнение, и деструктор еще не начал казнить. Точно так же, если не делегирующий конструктор для объекта завершил выполнение и делегирующий конструктор для этого объекта завершается с исключением, будет вызван деструктор объекта [обработанный как подобъект, как указано выше].

Это описывает делегирование согласованности ctors с моделью стека объектов C++, которая обязательно вводит накладные расходы.

Мне нужно было ознакомиться с такими вещами, как, как стек работает на аппаратном уровне, что такое указатель стека, что такое автоматические объекты и что такое раскрутка стека, чтобы действительно понять, как это работает. Технически эти термины / концепции являются деталями, определяемыми реализацией, поэтому N3242 не определяет ни одного из этих терминов; но он использует их.

Суть этого: объекты, объявленные в стеке, размещаются в памяти, а исполняемый файл обрабатывает адресацию и очистку за вас. Реализация стека была проста в C, но в C++ у нас есть исключения, и они требуют расширения разматывания стека C. В разделе 5 статьи Stroustrup * обсуждается необходимость раскручивания расширенного стека и необходимые дополнительные издержки, связанные с такой функцией:

Если у локального объекта есть деструктор, этот деструктор должен быть вызван как часть разматывания стека. [Расширение C++ для разматывания стека для автоматических объектов требует] ... техники реализации, которая (в дополнение к стандартным накладным расходам на установление обработчика) включает в себя только минимальные накладные расходы.

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

Это правда, что накладные расходы минимальны, и я уверен, что разумные реализации оптимизируют простые случаи, чтобы устранить эти накладные расходы. Однако рассмотрим случай, когда у вас есть цепочка наследования 5 классов. Допустим, у каждого из этих классов есть 5 конструкторов, и внутри каждого класса эти конструкторы вызывают друг друга в цепочке, чтобы уменьшить избыточное кодирование. Если вы создаете экземпляр экземпляра самого производного класса, вы будете подвергаться описанным выше накладным расходам до 25 раз, тогда как версия C++03 переносила бы эти накладные расходы до 10 раз. Если вы сделаете эти классы виртуальными и многократно наследуемыми, это увеличит накладные расходы, связанные с накоплением этих функций, а также тех функций, которые сами вводят дополнительные издержки. Мораль здесь в том, что когда ваш код масштабируется, вы почувствуете укус этой новой функции.

* Ссылка Stroustrup была написана давно, чтобы мотивировать обсуждение обработки исключений в C++ и определяет потенциальные (не обязательно) возможности языка C++. Я выбрал эту ссылку, а не какую-то конкретную реализацию, потому что она удобочитаема и "переносима". Основное использование этой статьи - раздел 5: в частности, обсуждение необходимости разматывания стека C++ и необходимости его накладных расходов. Эти концепции узаконены в статье и действительны сегодня для C++11.

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

...тело функции каждого делегирующего конструктора будет выполняться для полностью сконструированного объекта своего собственного типа.

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

В любом случае, поскольку выполняется только один список инициализаторов, таких издержек, как вы упомянули, нет. Ниже приводятся цитаты из cppreference:

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

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

Делегирующие конструкторы не могут быть рекурсивными.

Накладные расходы измеримы. Я реализовал следующиеmain-функция с Player-class и запускал его несколько раз с делегирующим конструктором, а также с конструктором с функцией init (закомментировано). Я построил код на g++ 7.5.0 и разных уровнях оптимизации.

Команда сборки: g++ -Ox main.cpp -s -o file_g++_Ox_(init|delegating).out

Я запускал каждую программу пять раз и вычислял среднее значение на процессоре Intel(R) Core(TM) i5-7200U @ 2,50 ГГц.

Время выполнения в мсек:

Opt-Level | делегирование | в этом

-O0 | 40966 | 26855

-O2 | 21868 | 10965

-O3 | 6475 | 5242

-Ofast | 6272 | 5123

Построить 50 000! объекты, вероятно, не обычный случай, но есть накладные расходы на конструкторы делегирования, и это был вопрос.

#include <chrono>

class Player
{
private:
    std::string name;
    int health;
    int xp;
public:
    Player();
    Player(std::string name_val, int health_val, int xp_val);
};

Player::Player()
    :Player("None", 0,0){
}

//Player::Player()
//        :name{"None"}, health{0},xp{0}{
//}

Player::Player(std::string name_val, int health_val, int xp_val)
    :name{name_val}, health{health_val},xp{xp_val}{

}

int main() {
    auto start = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < 50000; i++){
        Player player[i];
    }

    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>( end - start ).count();

    std::cout << duration;

    return 0;
}
Другие вопросы по тегам