未命名的匿名联合上的 [[no_unique_address]] 是否有效?

Is [[no_unique_address]] on an unnamed anonymous union valid?

tl;dr: 是

struct s {
  [[no_unique_address]] union {};
  [[no_unique_address]] union {};
};

有效吗?如果是,应该 sizeof(s) == 1?


我一直在研究各种技巧来解决 https://github.com/woboq/verdigris/issues/31。简而言之,问题是我需要在 class 函数声明之后放置一个宏,中间没有分号,这会混淆 clang-format。例如,给定:

#define W_SIGNAL(name) { /* body */ } static void name##Helper() {}

这个:

class widget {
public:
  void sig1() W_SIGNAL(sig1)
  void sig2() W_SIGNAL(sig2)
  void sig3() W_SIGNAL(sig3)
private:
  int i1;
  int i2;
  int i3;
};

格式化为

class widget {
public:
  void sig1() W_SIGNAL(sig1) void sig2() W_SIGNAL(sig2) void sig3()
      W_SIGNAL(sig3) private : int i1;
  int i2;
  int i3;
};

不幸的是,// clang-format off/on 并没有完全解决问题,因为 clang-format 似乎保留了禁用格式区域的上下文。例如,这个:

class widget {
public:
// clang-format off
  void sig1() W_SIGNAL(sig1)
  void sig2() W_SIGNAL(sig2)
  void sig3() W_SIGNAL(sig3) 
// clang-format on
private:
  int i1;
  int i2;
  int i3;
};

格式化为:

class widget {
public:
  // clang-format off
  void sig1() W_SIGNAL(sig1)
  void sig2() W_SIGNAL(sig2)
  void sig3() W_SIGNAL(sig3)
      // clang-format on
      private : int i1;
  int i2;
  int i3;
};

// clang-format on 需要放在 int i1 之后才能得到预期的行为,这有点不直观。

如果在 W_SIGNAL(<name>) 之后添加分号,

clang-format 会按预期格式设置,但这些分号会触发 -Wextra-semi 警告。额外的分号是相当无害的,因此可以毫不费力地禁用警告,但为了这个问题,让我们假设我无法禁用它。

在宏定义的末尾添加一个未命名的匿名联合允许在不触发的情况下添加结束分号-Wextra-semi:

#define W_SIGNAL(name) {} static void name##Helper() {} union {}

所以这个:

class widget {
public:
  void sig1() W_SIGNAL(sig1);
  void sig2() W_SIGNAL(sig2);
  void sig3() W_SIGNAL(sig3);
private:
  int i1;
  int i2;
  int i3;
};

按预期格式化,不触发 -Wextra-semi。 Clang 和 MSVC 确实会发出不同的警告(分别为 -Wmissing-declarations/C4408),但这些警告可以 _Pragma 在宏本身中消失,并且对用户有效不可见。

然而,匿名联合会改变包含 class 的大小,因此 sizeof(widget) > 3 * sizeof(int))。我很好奇 C++20 的 [[no_unique_address]] 属性是否可以在这里提供帮助,因为联合是空的并且可以想象地折叠在一起。当我tried it时,发现Clang接受了该属性并按我的预期进行了优化,但是GCC忽略了该属性并且MSVC拒绝了它。

海湾合作委员会:

<source>:1:66: warning: attribute ignored in declaration of 'union widget::<unnamed>' [-Wattributes]
    1 | #define W_SIGNAL(...) { /* body */ } [[no_unique_address]] union {}
      |                                                                  ^
<source>:3:15: note: in expansion of macro 'W_SIGNAL'
    3 |   void sig1() W_SIGNAL(sig1);
      |               ^~~~~~~~
<source>:1:66: note: attribute for 'union widget::<unnamed>' must follow the 'union' keyword
    1 | #define W_SIGNAL(...) { /* body */ } [[no_unique_address]] union {}
      |                                                                  ^
<source>:3:15: note: in expansion of macro 'W_SIGNAL'
    3 |   void sig1() W_SIGNAL(sig1);
      |   
<snip>
<source>:13:30: error: static assertion failed
   13 | static_assert(sizeof(widget) == 3 * sizeof(int));
      |               ~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~

MSVC:

<source>(3): error C7545: attribute 'no_unique_address' can only be applied to a non-static data member that is not a bitfield
<source>(4): error C7545: attribute 'no_unique_address' can only be applied to a non-static data member that is not a bitfield
<source>(5): error C7545: attribute 'no_unique_address' can only be applied to a non-static data member that is not a bitfield
<source>(13): error C2607: static assertion failed

给联合一个成员名称(例如,[[no_unique_address]] union {} W_MACRO_CONCAT(u, __LINE__))使代码有效,只有 MSVC 由于 static_assert 失败而发出错误。但是,这可能会污染 IDE/LSP 自动完成列表,这并不理想。

所以,忽略我出于某种原因而忽略的所有相对琐碎的解决方法,这里哪个编译器是正确的?

Is [[no_unique_address]] on an unnamed anonymous union valid?

似乎没有禁止它的规则。

should sizeof(s) == 1?

可以,但是“应该”有点太强了。它是实现定义的。

[intro.object]

An object has nonzero size if it ... Otherwise, if the object is a base class subobject ... Otherwise, the circumstances under which the object has zero size are implementation-defined.


至于格式化,联合技巧似乎是一个可怕的技巧。我推荐以下内容:

void sig1() { W_SIGNAL(sig1) }

鉴于宏为函数定义了主体,这也提高了可读性,因为正在定义函数变得很明显。

成员声明

attribute-specifier-seq(opt) decl-specifier-seq(opt) member-declarator-列表(选择);

attribute-specifier-seq属于被声明的实体,并且只允许出现在member-declarator-list 存在 ([class.mem.general]/14)。

这意味着表单

[[no_unique_address]] union {};

不允许作为成员声明。此声明中没有声明符。 union {} 是一个 decl-specifier-seq.

一般来说,一个属性说明符序列可能会在decl-specifiers之后出现在decl- specifier-seq,在这种情况下,它属于仅由该声明中的 decl-specifier-seq 确定的 type[dcl.spec.general]/1)。如果新类型由 decl-specifier 定义,如 structclassunionenumenum structenum class 定义,则在上述关键字序列之后也可能出现 属性说明符序列 ,并附加到定义的类型上。这两种情况都是病式的,因为 [[no_unique_address]] 可能只属于非静态数据成员。 C++ 语言的语法无法使 [[no_unique_address]] 属于匿名联合定义定义的未命名非静态数据成员。

即使这可行,这肯定是解决您的实际问题的一种奇怪方式。个人认为多加分号的方案更好