可以使用const变量避免混淆问题

Jon*_*Mee 6 c++ alias const strict-aliasing reinterpret-cast

我的公司使用消息服务器将消息const char*收集到a中,然后将其转换为消息类型.

在提出这个问题之后,我对此感到担忧.我不知道消息服务器中有任何不良行为.const变量是否可能不会出现锯齿问题?

例如,假设foo是以下列MessageServer方式之一定义的:

  1. 作为参数: void MessageServer(const char* foo)
  2. 或者作为顶部的const变量MessageServer:const char* foo = PopMessage();

现在MessageServer是一个巨大的功能,但它从来没有指派任何事情foo在1点但是,MessageServer的逻辑foo 被转换为所选择的消息类型.

auto bar = reinterpret_cast<const MessageJ*>(foo);
Run Code Online (Sandbox Code Playgroud)

bar 将仅从随后读取,但将广泛用于对象设置.

这里是否存在别名问题,或者foo只是初始化并且从未修改过的事实会保存我吗?

编辑:

Jarod42的回答中找到与从铸造没有问题const char*MessageJ*,但我不知道这是有道理的.

我们知道这是非法的:

MessageX* foo = new MessageX;
const auto bar = reinterpret_cast<MessageJ*>(foo);
Run Code Online (Sandbox Code Playgroud)

我们是否以某种方式说这是合法的?

MessageX* foo = new MessageX;
const auto temp = reinterpret_cast<char*>(foo);
auto bar = reinterpret_cast<const MessageJ*>(temp);
Run Code Online (Sandbox Code Playgroud)

我对Jarod42答案的理解是演员表明temp它是合法的.

编辑:

关于序列化,对齐,网络传递等等,我得到了一些评论.这不是这个问题的关键所在.

这是一个关于严格别名的问题.

严格别名是由C(或C++)编译器做出的假设,即取消引用指向不同类型对象的指针永远不会引用相同的内存位置(即别名的别名).

我要问的是:对象的初始化是否会在下面的对象被转换为另一种类型的对象时进行优化,以便从未初始化的数据中进行转换?constchar*

jsc*_*410 2

我的公司使用消息传递服务器,它将消息获取到 const char* 中,然后将其转换为消息类型。

只要你的意思是它执行reinterpret_cast(或转变成reinterpret_cast的C风格转换):

MessageJ *j = new MessageJ();

MessageServer(reinterpret_cast<char*>(j)); 
// or PushMessage(reinterpret_cast<char*>(j));
Run Code Online (Sandbox Code Playgroud)

然后使用相同的指针并将其reinterpret_cast返回到实际的底层类型,那么该过程是完全合法的:

MessageServer(char *foo)
{
  if (somehow figure out that foo is actually a MessageJ*)
  {
    MessageJ *bar = reinterpret_cast<MessageJ*>(foo);
    // operate on bar
  }      
}

// or

MessageServer()
{
  char *foo = PopMessage();

  if (somehow figure out that foo is actually a MessageJ*)
  {
    MessageJ *bar = reinterpret_cast<MessageJ*>(foo);
    // operate on bar
  }      
}
Run Code Online (Sandbox Code Playgroud)

请注意,我特意从您的示例中删除了 const,因为它们的存在或不存在并不重要。foo当指向的底层对象实际上是a时,上述内容是合法的MessageJ,否则是未定义的行为。再次进行reinterpret_cast'ingchar*和返回会产生原始类型化指针。事实上,您可以将reinterpret_cast 转换为任何类型的指针,然后再返回并获取原始类型化指针。从这个参考

只有以下转换可以用reinterpret_cast 完成...

6) 类型 T1 的左值表达式可以转换为对另一个类型 T2 的引用。结果是左值或x值引用与原始左值相同的对象,但类型不同。不创建临时文件,不进行复制,不调用构造函数或转换函数。只有在类型别名规则允许的情况下才能安全地访问生成的引用(见下文)...

类型别名

当对类型 T1 的对象的指针或引用进行reinterpret_cast(或 C 风格转换)到对不同类型 T2 的对象的指针或引用时,转换始终会成功,但仅当 T1和 T2 是标准布局类型,且以下条件之一为真:

  • T2 是对象的(可能是 cv 限定的)动态类型...

实际上,不同类型的指针之间的reinterpret_cast'ing只是指示编译器将指针重新解释为指向不同的类型。但对于您的示例来说,更重要的是,再次往返回原始类型然后对其进行操作是安全的。这是因为您所做的只是指示编译器将指针重新解释为指向不同的类型,然后再次告诉编译器将同一指针重新解释为指向原始的基础类型。

因此,指针的往返转换是合法的,但是潜在的别名问题又如何呢?

这里是否可能存在别名问题,或者 foo 仅被初始化且从未被修改这一事实拯救了我?

严格的别名规则允许编译器假设对不相关类型的引用(和指针)不引用相同的底层内存。此假设允许进行大量优化,因为它将不相关引用类型上的操作解耦为完全独立。

#include <iostream>

int foo(int *x, long *y)  
{
  // foo can assume that x and y do not alias the same memory because they have unrelated types
  // so it is free to reorder the operations on *x and *y as it sees fit
  // and it need not worry that modifying one could affect the other
  *x = -1;
  *y =  0;
  return *x;
}

int main()
{
  long a;
  int  b = foo(reinterpret_cast<int*>(&a), &a);  // violates strict aliasing rule

  // the above call has UB because it both writes and reads a through an unrelated pointer type
  // on return b might be either 0 or -1; a could similarly be arbitrary
  // technically, the program could do anything because it's UB

  std::cout << b << ' ' << a << std::endl;

  return 0;
}
Run Code Online (Sandbox Code Playgroud)

在此示例中,由于严格的别名规则,编译器可以假设foo该设置*y不会影响 的值*x。因此,例如,它可以决定仅返回 -1 作为常量。如果没有严格的别名规则,编译器将不得不假设更改*y实际上可能会更改 的值*x。因此,它必须强制执行给定的操作顺序并*x在设置后重新加载*y。在这个例子中,强制执行这种偏执看起来似乎足够合理,但在不那么琐碎的代码中,这样做将极大地限制操作的重新排序和消除,并迫使编译器更频繁地重新加载值。

以下是我以不同方式编译上述程序时在我的机器上的结果(Apple LLVM v6.0 for x86_64-apple-darwin14.1.0):

$ g++ -Wall test58.cc
$ ./a.out
0 0
$ g++ -Wall -O3 test58.cc
$ ./a.out
-1 0
Run Code Online (Sandbox Code Playgroud)

在您的第一个示例中,foo是 aconst char *并且barconst MessageJ *来自 的reinterpret_cast'ed foo。您进一步规定该对象的基础类型实际上是 aMessageJ并且不通过 进行读取const char *。相反,它仅被转换为const MessageJ *仅从中进行读取的内容。由于您不通过const char *别名进行读取或写入,因此首先通过第二个别名进行访问时不会出现别名优化问题。这是因为通过不相关类型的别名在底层内存上执行的操作不会存在潜在冲突。但是,即使您确实通读了foo,仍然可能没有潜在的问题,因为类型别名规则允许此类访问(见下文),并且任何通读顺序foobar都会产生相同的结果,因为这里没有发生写入。

现在让我们从您的示例中删除 const 限定符,并假设它MessageServer确实执行了一些写入操作bar,而且该函数还foo出于某种原因进行读取(例如 - 打印内存的十六进制转储)。通常,这里可能存在别名问题,因为我们通过不相关的类型通过两个指向同一内存的指针进行读取和写入。foo然而,在这个特定的例子中,我们被a 的事实所拯救char*,它得到了编译器的特殊处理:

类型别名

当对类型 T1 的对象的指针或引用进行reinterpret_cast(或 C 风格转换)到对不同类型 T2 的对象的指针或引用时,转换始终会成功,但仅当 T1和 T2 是标准布局类型,且以下条件之一为真:...

  • T2 是 char 或 unsigned char

当引用(或指针)起作用时,明确禁止通过不相关类型的引用(或指针)进行操作的严格别名优化。char相反,编译器必须偏执地认为通过char引用(或指针)进行的操作可能会影响通过其他引用(或指针)完成的操作,并且受到通过其他引用(或指针)完成的操作的影响。在修改后的示例中,读取和写入都对foo和进行操作bar,您仍然可以具有定义的行为,因为foochar*。因此,不允许编译器以与编写的代码的串行执行相冲突的方式进行优化以重新排序或消除对两个别名的操作。同样,它被迫对重新加载可能受到通过任一别名的操作影响的值持偏执态度。

你的问题的答案是,只要你的函数是通过返回char*其原始类型正确地往返指向类型的指针,那么你的函数就是安全的,即使你要交错读取(以及可能的写入,请参阅警告)编辑结束)通过char*别名,通过底层类型别名进行读+写。

两个技术参考文献 (3.10.10)对于回答您的问题很有用。 这些其他参考资料有助于更好地理解技术信息。

====
编辑:在下面的评论中, zmb 对象虽然char*可以合法地别名不同的类型,但反之亦然,因为几个来源似乎以不同的形式表示:char*严格别名规则的例外是不对称的, “单向”规则。

让我们修改上面的严格别名代码示例,并询问这个新版本是否会同样导致未定义的行为?

#include <iostream>

char foo(char *x, long *y)
{
  // can foo assume that x and y cannot alias the same memory?
  *x = -1;
  *y =  0;
  return *x;
}

int main()
{
  long a;
  char b = foo(reinterpret_cast<char*>(&a), &a);  // explicitly allowed!

  // if this is defined behavior then what must the values of b and a be?

  std::cout << (int) b << ' ' << a << std::endl;

  return 0;
}
Run Code Online (Sandbox Code Playgroud)

我认为这是定义的行为,并且在调用 后 a 和 b 都必须为零foo。来自C++ 标准 (3.10.10)

如果程序尝试通过以下类型之一以外的泛左值访问对象的存储值,则行为未定义:^52

  • 对象的动态类型...

  • char 或 unsigned char 类型...

^52:此列表的目的是指定对象可以或不可以使用别名的情况。

在上面的程序中,我通过对象的实际类型和 char 类型访问对象的存储值,因此它是定义的行为,并且结果必须与所编写的代码的串行执行相一致。

现在,编译器没有通用的方法可以始终静态地知道foo指针x实际上y是否是别名(例如 - 想象一下是否foo在库中定义)。也许程序可以在运行时通过检查指针本身的值或咨询 RTTI 来检测此类别名,但这样做所产生的开销是不值得的。相反,当和确实发生别名时,一般编译foo并允许定义行为的更好方法是始终假设它们可以(即 - 当 a正在运行时禁用严格的别名优化)。xychar*

以下是我编译并运行上述程序时发生的情况:

$ g++ -Wall test59.cc
$ ./a.out
0 0
$ g++ -O3 -Wall test59.cc
$ ./a.out
0 0
Run Code Online (Sandbox Code Playgroud)

此输出与早期的类似严格别名程序的输出不一致。这并不是证明我对标准的正确性的决定性证据,但同一编译器的不同结果提供了很好的证据,证明我可能是正确的(或者,至少一个重要的编译器似乎以相同的方式理解标准)。

让我们检查一些看似 相互矛盾的 来源

反之则不然。将 char* 转换为 char* 以外的任何类型的指针并取消引用它通常会违反严格的别名规则。换句话说,通过 char*从一种类型的指针转​​换为不相关类型的指针是未定义的。

粗体部分是为什么这句话不适用于我的答案所解决的问题或我刚刚给出的示例。在我的答案和示例中,别名内存都是通过char*对象本身的实际类型访问的,这可以是定义的行为。

C 和 C++ 都允许通过 char *(或者具体来说,char 类型的左值)访问任何对象类型。它们不允许通过任意类型访问char 对象。所以是的,这个规则是一个“单向”规则。”

同样,粗体部分是为什么这个陈述不适用于我的答案。在这个例子和类似的反例中,通过不相关类型的指针访问字符数组。即使在 C 中,这也是 UB,因为字符数组可能不会根据别名类型的要求进行对齐。在 C++ 中,这是 UB,因为此类访问不满足任何类型别名规则,因为对象的基础类型实际上是char

在我的示例中,我们首先有一个指向正确构造的类型的有效指针,然后用 achar*别名,然后通过这两个别名指针进行交错读写,这可以定义行为。char因此,严格别名异常和不通过不兼容引用访问底层对象之间似乎存在一些混淆和混淆。

int   value;  
int  *p = &value;  
char *q = reinterpret_cast<char*>(&value);
Run Code Online (Sandbox Code Playgroud)

p 和 p 都引用相同的地址,它们是同一内存的别名。该语言所做的是提供一组规则,定义所保证的行为:通过 p 写入,通过 q 读取很好,其他方式不好

标准和许多示例明确指出“写入 q,然后读取 p(或值)”可以是明确定义的行为。虽然不太清楚,但我在这里争论的是,“通过 p(或值)写入,然后通过 q 读取”始终是明确定义的。我进一步声称,“通过 p(或值)进行的读取和写入可以与对 q 的读取和写入任意交错”,具有明确定义的行为。

现在对前面的陈述有一个警告,以及为什么我在上面的文本中不断散布“可以”这个词。如果您有一个类型T引用和一个char对同一内存别名的引用,那么对引用的读+写T与对引用的读取的任意交错始终char是明确定义的。例如,当您通过引用多次修改底层内存时,您可以执行此操作以重复打印底层内存的十六进制转储。该标准保证严格的别名优化不会应用于这些交错访问,否则可能会给您带来未定义的行为。T

但是通过引用别名写入又如何呢char?嗯,这样的写入可能会也可能不会被明确定义。如果通过引用进行写入char违反了基础T类型的不变量,那么您可能会得到未定义的行为。如果这样的写入不正确地修改了成员指针的值T,那么您可能会得到未定义的行为。如果这样的写入将T成员值修改为陷阱值,那么您可能会得到未定义的行为。等等。然而,在其他情况下,char可以完全很好地定义通过引用的写入。例如,通过别名引用读+写来重新排列 auint32_t或 的字节顺序总是被明确定义的。因此,此类写入是否完全明确定义取决于写入本身的细节。无论如何,该标准保证其严格的别名优化不会以本身可能导致未定义行为的方式重新排序或消除别名内存上的其他操作的此类写入。uint64_tchar