Я начинаю С++ и хочу понять, почему
return std::list<int>();
нужны скобки, но
std::list<int> foo;
не требуется скобок. Какая разница между этими конструкторскими вызовами?
Я начинаю С++ и хочу понять, почему
return std::list<int>();
нужны скобки, но
std::list<int> foo;
не требуется скобок. Какая разница между этими конструкторскими вызовами?
Ни один из них не является вызовом конструктора.
Первым является явное преобразование типов, которое создает объект типа std::list<int>
.
Второе - это определение переменной, которое создает объект типа std::list<int>
.
Конструктор по умолчанию (конструктор, не принимающий аргументов) в обоих случаях называется частью создания.
Хотя вы можете видеть, что такие вещи упоминаются как "вызовы конструктора", нет синтаксической конструкции для явного и однозначного вызова конструктора в С++.
Причина, по которой нужны скобки, когда другая не существует, состоит в том, что они представляют собой две отдельные языковые конструкции с различным синтаксисом, а не два способа вызова конструктора.
Обратите внимание, что если вы добавите круглые скобки во второй пример, вы фактически объявляете функцию, а не определяете переменную:
std::list<int> foo; //variable definition
std::list<int> foo(); //function taking no args, returning a std::list<int>
Это обычно называется most-vexing-parse. С++ 11 ввел исправленную инициализацию, чтобы обойти это:
std::list<int> foo{}; //variable definition
(Цитаты из N3337)
"Но T()
обязательно выглядит как вызов конструктора, почему это не так?"
В этом контексте T()
называется явным преобразованием типа с функциональными обозначениями:
5.2.3 Явное преобразование типа (функциональная нотация) [expr.type.conv]
1 [...]
2 Выражение
T()
, гдеT
- спецификатор простого типа или typename-specifier для типа объекта без массива или (возможно, cv-qualified) тип void, создает значение prvalue указанного типа, который инициализируется значением (8.5; инициализация не выполняется для случая void()). [Примечание: еслиT
- тип неклассов, который cv-qualified, cv-квалификаторы игнорируются при определении типа полученного prvalue (3.10). -end note]
Таким образом, это создает значение prvalue, которое инициализируется значением.
[dcl.init]/7:
Для инициализации объекта с типомT
означает:- , если T - тип класса (возможно, cv-grade) (раздел 9) с предоставленным пользователем конструктором (12.1), тогда конструктор по умолчанию для T называется (и инициализация плохо сформирован, если T не имеет доступных значений по умолчанию Конструктор);
- [...]
Таким образом, это вызывает конструктор как часть инициализации значения, которая является частью явного преобразования типов. Как указано выше, невозможно напрямую вызвать конструктор. В стандарте говорится:
[class.ctor]/1:
Конструкторы не имеют имен. Для объявления или определения конструктора используется специальный синтаксис декларатора. Синтаксис использует:- необязательный spec-specifier-seq, в котором каждый spec-спецификатор является либо спецификатором функции, либо constexpr,
- имя класса конструкторов и
- список параметров
в этом порядке. В таком объявлении необязательные круглые скобки вокруг имени класса конструктора игнорируются.
Поэтому у конструкторов нет имен, и мы объявляем/определяем их с исключением синтаксиса, который определяет язык.
"Это похоже на академическое различие, действительно ли это на практике?"
Может быть, может и нет. Мое мнение заключается в том, что интерпретация синтаксиса, подобная приведенному выше, как вызов чистого конструктора, рисует неверную картину того, что такое конструктор. Конструктор инициализирует объект; он не выделяет эту память объекта, возвращает инициализированный объект, связывает символ с этим объектом или что-то еще, что делается с помощью определений переменных и преобразований типов. Кроме того, он может создавать путаницу, подобную той, что существует в OP, который ожидал единообразного синтаксиса, потому что он думал, что эти две конструкции являются вызовами конструктора.
Зачем использовать неточный synecdoche, когда у нас есть формальные термины, которые позволяют избежать путаницы?
посмотрите на это следующим образом:
1) вам нужно создать объект
2) вам нужно вернуть его.
скажем, что компилятор смотрит на выражение return Foo;
, компилятор думает: "Эй, он хочет, чтобы я возвращал тип! Тип - это не то, что я могу вернуть! Мне нужна настоящая переменная здесь!"
чтобы вы могли написать что-то вроде
Foo temp;
return temp;
или сделать его короче - вызовите конструктор по умолчанию Foo
, а затем верните анонимный объект, который я только что создал. вы обрабатываете конструктор как функцию, которая создает объект.
выглядит ли код return createDefaultFoo();
более разумным? ну, это то, что делает Foo()
, он создает и возвращает анонимный Foo
obejct
в этой строке:
std::list<int> foo;
компилятор может сказать, что вам нужен объект с именем Foo
от типа std::list<int>
. поэтому ()
redundand. как сказано здесь, добавление ()
заставит компилятор считать, что вы объявляете функцию.
Оба оператора ссылаются на конструктор по умолчанию.
return std::list<int>();
Это то же самое, что:
std::list<int> value;
return value;
Здесь создается объект (с использованием конструктора по умолчанию) и возвращается объект.
std::list<int> foo;
Здесь объект foo
создается с использованием конструктора по умолчанию.
Вот еще один способ сделать то же самое в C++11
:
std::list<int> foo;
std::list<int> foo1{}; // C++11