使用vs. typedef - 是否有一个微妙的,鲜为人知的区别?

jba*_*bab 23 gcc language-lawyer c++11 clang++ c++14

背景

每个人都同意这一点

using <typedef-name> = <type>;
Run Code Online (Sandbox Code Playgroud)

相当于

typedef <type> <typedef-name>;
Run Code Online (Sandbox Code Playgroud)

并且前者因各种原因而优先于后者(参见Scott Meyers,Effective Modern C++和stackoverflow上的各种相关问题).

这是由[dcl.typedef]支持的:

也可以通过别名声明引入typedef-name.在使用关键字以下的标识符成为typedef名以及可选的以下标识符appertains到的typedef名称属性说明符-SEQ.这样的typedef-name具有与typedef说明符引入的语义相同的语义.

但是,请考虑诸如此类的声明

typedef struct {
    int val;
} A;
Run Code Online (Sandbox Code Playgroud)

对于这种情况,[dcl.typedef]指定:

如果typedef声明定义了一个未命名的类(或枚举),则声明声明为该类类型(或枚举类型)的第一个typedef-name用于表示仅用于链接目的的类类型(或枚举类型)(3.5 ).

参考部分3.5 [basic.link]说

如果名称是在typedef声明中定义的未命名类的名称,那么具有未在上面给出内部链接的命名空间范围的名称与封闭命名空间具有相同的链接,其中类具有用于链接的typedef名称用途[...]

假设上面的typedef声明是在全局命名空间中完成的,那么struct A将具有外部链接,因为全局命名空间具有外部链接.

现在的问题是,如果typedef声明被别名声明替换,根据它们是等效的常见概念,是否也是如此:

using A = struct {
    int val;
};
Run Code Online (Sandbox Code Playgroud)

特别是,A通过别名声明("using")声明的类型与通过typedef声明声明的类型具有相同的链接吗?

请注意,[decl.typedef]并未说别名声明 typedef声明(它只表示两者都引入了typedef-name),并且[decl.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
Run Code Online (Sandbox Code Playgroud)

a.cpp

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

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

main.cpp中

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

int main()
{
    A a;
    a.val = 42;
    print(std::cout, a);
}
Run Code Online (Sandbox Code Playgroud)

GCC

使用带有"typedef"变体的gcc 7.2进行编译,可以干净地编译并提供预期的输出:

> g++ -Wall -Wextra -pedantic-errors -DUSING_VS_TYPEDEF=0 a.cpp main.cpp
> ./a.out
42
Run Code Online (Sandbox Code Playgroud)

使用"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);
     ^~~~~
Run Code Online (Sandbox Code Playgroud)

这看起来像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
Run Code Online (Sandbox Code Playgroud)

因此,人们也可以问

  • 哪个编译器正确?

小智 10

这看起来像是GCC中的一个错误.

请注意,[decl.typedef]并未说别名声明是typedef声明

你是对的,[dcl.dcl] p9给出了术语typedef声明的定义,它排除了别名声明 s.但是,正如您在问题中引用的那样,[dcl.typedef]会明确说出:

2 别名声明也可以引入typedef-name.的标识符下面的关键字成为typedef的名称和可选属性说明符-SEQ标识符 appertains到的typedef名.它具有与说明符引入的语义相同的语义.[...]usingtypedef

"同样的语义"并没有留下任何疑问.根据海湾合作委员会的解释,typedef并且using具有不同的语义,因此唯一合理的结论是海湾合作委员会的解释是错误的.应用于typedef声明的任何规则都必须解释为也适用于别名声明.

  • typedef-name是一回事,typedef声明是另一回事.别名声明引入了typedef-name,但不是typedef声明.后者在标准中非常精确地定义为包含关键字"typedef"的声明.标准中有很多地方谈论"typedef声明或*别名声明*"(注意连字符,斜体和"a"的用法,我已经从草案标准中精确复制了它们). (2认同)