используя против typedef - есть ли тонкая, менее известная разница?

Фон

Все согласны с тем, что

using <typedef-name> = <type>;

эквивалентно

typedef <type> <typedef-name>;

и что первое должно быть предпочтительнее последнего по разным причинам (см. Scott Meyers, "Эффективное современное" C++ и различные связанные с ним вопросы о потоке stackoverflow).

Это подтверждается [dcl.typedef]:

Имя typedef также может быть введено с помощью объявления alias. Идентификатор, следующий за ключевым словом using, становится typedef-name и необязательным атрибутом-спецификатором-seq, следующим за идентификатором, к этому typedef-name. Такое имя typedef имеет такую же семантику, как если бы он был введен спецификатором typedef.

Однако рассмотрите декларацию, такую как

typedef struct {
    int val;
} A;

Для этого случая [dcl.typedef] указывает:

Если декларация typedef определяет неназванный класс (или перечисление), первое имя typedef, объявленное объявлением как этот тип класса (или тип перечисления), используется для обозначения типа класса (или типа перечисления) только для целей привязки (3.5).

В упомянутом разделе 3.5 [basic.link] говорится

Имя с областью пространства имен, которая не была предоставлена внутренней связью выше, имеет ту же связь, что и охватывающее пространство имен, если оно является [...] неназванным классом, определенным в объявлении typedef, в котором класс имеет имя typedef для привязки целей [...]

Предполагая, что объявление typedef выше сделано в глобальном пространстве имен, структура A будет иметь внешнюю связь, поскольку глобальное пространство имен имеет внешнюю связь.

Вопрос

Вопрос в том, является ли то же самое истинным, если объявление typedef заменяется объявлением псевдонима в соответствии с общим понятием, что они эквивалентны:

using A = struct {
    int val;
};

В частности, имеет ли тип A объявленный через объявление псевдонима ("использование"), те же связи, что и декларация, указанная в объявлении typedef?

Обратите внимание, что [decl.typedef] не говорит, что объявление псевдонима является объявлением typedef (оно только говорит, что оба вводят имя typedef), и что [decl.typedef] говорит только о объявлении typedef (а не об объявлении псевдонима), имеющем свойство введения имени typedef для целей связывания. Если объявление псевдонима не способно вводить имя typedef для целей привязки, A будет просто псевдонимом анонимного типа и вообще не имеет привязки.

ИМО, по крайней мере, одну возможную, хотя и строгую, интерпретацию стандарта. Конечно, я, возможно, что-то пропускаю.

Это вызывает следующие вопросы:

  • Если действительно есть эта тонкая разница, это по намерению или это надзор в стандарте?
  • Каково ожидаемое поведение компиляторов/линкеров?

Исследование

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

a.hpp

#ifndef A_HPP
#define A_HPP

#include <iosfwd>

#if USING_VS_TYPEDEF
using A = struct {
     int val;
};
#else
typedef struct {
     int val;
} A;
#endif

void print(std::ostream& os, A const& a);

#endif // A_HPP

a.cpp

#include "a.hpp"
#include <iostream>

void print(std::ostream& os, A const& a)
{
   os << a.val << "\n";
}

main.cpp

#include "a.hpp"
#include <iostream>

int main()
{
    A a;
    a.val = 42;
    print(std::cout, a);
}

НКУ

Компиляция этого с gcc 7.2 с вариантом "typedef" компилируется чисто и обеспечивает ожидаемый результат:

> g++ -Wall -Wextra -pedantic-errors -DUSING_VS_TYPEDEF=0 a.cpp main.cpp
> ./a.out
42

Компиляция с вариантом "using" создает ошибку компиляции:

> g++ -Wall -Wextra -pedantic-errors -DUSING_VS_TYPEDEF=1 a.cpp main.cpp
a.cpp:4:6: warning: ‘void print(std::ostream&, const A&) defined but not used [-Wunused-function]
void print(std::ostream& os, A const& a)
     ^~~~~
In file included from main.cpp:1:0:
a.hpp:16:6: error: ‘void print(std::ostream&, const A&), declared using unnamed type, is used but never defined [-fpermissive]
void print(std::ostream& os, A const& a);
     ^~~~~
a.hpp:9:2: note: ‘using A = struct<unnamed> does not refer to the unqualified type, so it is not used for linkage
};
 ^
a.hpp:16:6: error: ‘void print(std::ostream&, const A&) used but never defined
void print(std::ostream& os, A const& a);
     ^~~~~

Это похоже на то, что GCC следует строгой интерпретации вышеприведенного стандарта и делает разницу в отношении связи между typedef и декларацией псевдонима.

лязг

Используя clang 6, оба варианта компилируются и запускаются без предупреждения:

> clang++ -Wall -Wextra -pedantic-errors -DUSING_VS_TYPEDEF=0 a.cpp main.cpp
> ./a.out
42

> clang++ -Wall -Wextra -pedantic-errors -DUSING_VS_TYPEDEF=1 a.cpp main.cpp
> ./a.out
42

Поэтому можно было бы также спросить

  • Какой компилятор прав?

Ответ 1

Это выглядит как ошибка в GCC.

Обратите внимание, что [decl.typedef] не говорит, что объявление псевдонима является объявлением typedef

Вы правы, [dcl.dcl] p9 дает определение термина typedef, которое исключает псевдонимы-декларации. Однако [dcl.typedef] явно говорит, как вы цитировали в своем вопросе:

2 Имя typedef также может быть введено с помощью объявления alias. Идентификатор, следующий за ключевым словом using становится typedef-name и необязательным атрибутом-спецификатором-seq, следующим за идентификатором, к этому typedef-name. Он имеет ту же семантику, как если бы он был введен спецификатором typedef. [...]

"Та же семантика" не оставляет никаких сомнений. В интерпретации GCC typedef и using имеют разную семантику, поэтому единственным разумным заключением является то, что интерпретация GCC неверна. Любые правила, применяемые к объявлениям typedef, должны интерпретироваться как применимые к объявлениям псевдонимов.

Ответ 2

Похоже, что на этом неясно.

С одной стороны,

[dcl.typedef] Имя typedef также может быть введено с помощью объявления alias. [...] Такое имя typedef имеет такую же семантику, как если бы он был введен спецификатором typedef.

С другой стороны, стандарт четко разделяет понятия декларации typedef и декларации псевдонимов (последний термин - название производной грамматики, поэтому оно выделено курсивом и переносится, а первое - нет). В некоторых контекстах он говорит о "объявлении typedef или alias-declaration", что делает их эквивалентными в этих контекстах; и иногда речь идет исключительно о "объявлении typedef". В частности, всякий раз, когда в стандарте говорится о ссылках и объявлениях typedef, он говорит только о объявлениях typedef и не упоминает объявление-псевдоним. Это включает в себя ключевой проход

[dcl.typedef] Если декларация typedef определяет неназванный класс (или перечисление), первое имя typedef, объявленное объявлением как этот тип класса (или тип перечисления), используется для обозначения типа класса (или типа перечисления) для только для связи.

Обратите внимание, что стандарт настаивает на первом имени typedef, которое используется для связи. Это означает, что в

typedef struct { int x; } A, B;

для связывания используется только A, а B - нет. Ничто в стандарте не указывает, что имя, введенное с помощью объявления псевдонима, должно вести себя как A а не как B

По моему мнению, этот стандарт недостаточно ясен в этой области. Если целью является создание только объявления typedef для привязки, тогда было бы уместно явно указать в [dcl.typedef], что объявление alias не делает этого. Если цель состоит в том, чтобы сделать работу по декларированию псевдонимов для связи, это должно быть указано явно, как это сделано в других контекстах.