使用 const 变量可以避免别名问题吗

Can Aliasing Problems be Avoided with const Variables

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

在询问 之后,我开始担心这个问题。我不知道消息传递服务器中有任何不良行为。 const 变量是否可能不会产生别名问题?

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

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

现在 MessageServer 是一个巨大的函数,但它从不给 foo 分配任何东西,但是在 MessageServer 的逻辑 foo [=65= 的 1 点]将转换为选定的消息类型。

auto bar = reinterpret_cast<const MessageJ*>(foo);

bar 只会在后续读取,但会广泛用于对象设置。

这里可能存在别名问题,还是 foo 仅初始化而从未修改的事实拯救了我?

编辑:

发现从 const char* 转换为 MessageJ* 没有问题,但我不确定这是否有意义。

我们知道这是非法的:

MessageX* foo = new MessageX;
const auto bar = reinterpret_cast<MessageJ*>(foo);

我们是说这在某种程度上使其合法吗?

MessageX* foo = new MessageX;
const auto temp = reinterpret_cast<char*>(foo);
auto bar = reinterpret_cast<const MessageJ*>(temp);

我对 的理解是转换为 temp 使其合法。

编辑:

我收到了一些关于序列化、对齐、网络传递等方面的评论。这不是这个问题的目的。

这是一个关于strict aliasing的问题。

Strict aliasing is an assumption, made by the C (or C++) compiler, that dereferencing pointers to objects of different types will never refer to the same memory location (i.e. alias eachother.)

我要问的是:const 对象的初始化(通过从 char* 转换)是否会在该对象转换为另一个对象的位置以下进行优化对象的类型,以便我从未初始化的数据中转换?

使用(const)char*类型没有别名问题,见最后一点:

If a program attempts to access the stored value of an object through a glvalue of other than one of the following types the behavior is undefined:

  • the dynamic type of the object,
  • a cv-qualified version of the dynamic type of the object,
  • a type similar (as defined in 4.4) to the dynamic type of the object,
  • a type that is the signed or unsigned type corresponding to the dynamic type of the object,
  • a type that is the signed or unsigned type corresponding to a cv-qualified version of the dynamic type of the object,
  • an aggregate or union type that includes one of the aforementioned types among -its elements or non-static data members (including, recursively, an element or non-static data member of a subaggregate or contained union),
  • a type that is a (possibly cv-qualified) base class type of the dynamic type of the object,
  • a char or unsigned char type.

另一个答案很好地回答了这个问题(它直接引用了 https://isocpp.org/files/papers/N3690.pdf 第 75 页中的 C++ 标准),所以我将指出您所做的其他问题。

请注意,您的代码可能 运行 出现对齐问题。例如,如果 MessageJ 的对齐是 4 或 8 个字节(典型的在 32 位和 64 位机器上),严格来说,将任意字符数组指针作为 MessageJ 指针访问是未定义的行为。

您不会 运行 在 x86/AMD64 体系结构上遇到任何问题,因为它们允许未对齐的访问。但是,有一天您可能会发现您正在开发的代码被移植到移动 ARM 架构上,那么未对齐的访问就会成为一个问题。

因此看起来你正在做一些你不应该做的事情。我会考虑使用序列化而不是将字符数组作为 MessageJ 类型进行访问。唯一的问题不是潜在的对齐问题,另一个问题是数据在 32 位和 64 位架构上可能具有不同的表示形式。

My company uses a messaging server which gets a message into a const char* and then casts it to the message type.

只要你的意思是它做一个 reinterpret_cast(或一个 C 风格的类型转换为 reinterpret_cast):

MessageJ *j = new MessageJ();

MessageServer(reinterpret_cast<char*>(j)); 
// or PushMessage(reinterpret_cast<char*>(j));

之后使用 相同的 指针并将 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
  }      
}

请注意,我特别从您的示例中删除了常量,因为它们存在与否并不重要。当 foo 指向 的底层对象实际上是 一个 MessageJ 时,上述是合法的,否则是未定义的行为。 reinterpret_cast 到 char* 并再次返回会产生原始类型指针。实际上,您可以 reinterpret_cast 指向 any 类型的指针并再次返回并获得原始类型指针。来自 this reference:

Only the following conversions can be done with reinterpret_cast ...

6) An lvalue expression of type T1 can be converted to reference to another type T2. The result is an lvalue or xvalue referring to the same object as the original lvalue, but with a different type. No temporary is created, no copy is made, no constructors or conversion functions are called. The resulting reference can only be accessed safely if allowed by the type aliasing rules (see below) ...

Type aliasing

When a pointer or reference to object of type T1 is reinterpret_cast (or C-style cast) to a pointer or reference to object of a different type T2, the cast always succeeds, but the resulting pointer or reference may only be accessed if both T1 and T2 are standard-layout types and one of the following is true:

  • T2 is the (possibly cv-qualified) dynamic type of the object ...

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

那么,你的指针的往返转换是合法的,但是潜在的别名问题呢?

Is an aliasing problem possible here, or does the fact that foo is only initialized, and never modified save me?

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

#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;
}

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

以下是我以不同方式编译上述程序时在我的机器上的结果(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

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

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

Type aliasing

When a pointer or reference to object of type T1 is reinterpret_cast (or C-style cast) to a pointer or reference to object of a different type T2, the cast always succeeds, but the resulting pointer or reference may only be accessed if both T1 and T2 are standard-layout types and one of the following is true: ...

  • T2 is char or unsigned char

char 引用(或指针)是在玩。相反,编译器必须偏执地认为通过 char 引用(或指针)的操作会影响通过其他引用(或指针)完成的操作并受其影响。在对 foobar 进行读写操作的修改示例中,您仍然可以定义行为,因为 foochar*。因此,不允许编译器以与编写的代码的串行执行冲突的方式优化以重新排序或消除对您的两个别名的操作。同样,它被迫偏执地重新加载可能已通过任一别名的操作影响的值。

你的问题的答案是,只要你的函数正确地将指向一个类型的指针通过 char* 返回到它的原始类型,那么你的函数就是安全的,即使你要通过 char* 别名交错读取(并可能写入,请参阅编辑末尾的警告),通过基础类型别名进行读取+写入。

These two technical references (3.10.10) are useful for answering your question. These other references 有助于更好地理解技术信息。

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

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

#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;
}

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

If a program attempts to access the stored value of an object through a glvalue of other than one of the following types the behavior is undefined:^52

  • the dynamic type of the object ...

  • a char or unsigned char type ...

^52: The intent of this list is to specify those circumstances in which an object may or may not be aliased.

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

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

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

$ g++ -Wall test59.cc
$ ./a.out
0 0
$ g++ -O3 -Wall test59.cc
$ ./a.out
0 0

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

让我们检查一些 seemingly sources:

The converse is not true. Casting a char* to a pointer of any type other than a char* and dereferencing it is usually in volation of the strict aliasing rule. In other words, casting from a pointer of one type to pointer of an unrelated type through a char* is undefined.

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

Both C and C++ allow accessing any object type via char * (or specifically, an lvalue of type char). They do not allow accessing a char object via an arbitrary type. So yes, the rule is a "one way" rule."

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

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

int   value;  
int  *p = &value;  
char *q = reinterpret_cast<char*>(&value);

Both p and p refer to the same address, they are aliasing the same memory. What the language does is provide a set of rules defining the behaviors that are guaranteed: write through p read through q fine, other way around not fine.

标准和许多示例清楚地表明 "write through q, then read through p (or value)" 可以是定义明确的行为。不是很清楚,但我在这里争论的是 "write through p (or value), then read through q" 是 always 明确定义的。我进一步声称,"reads and writes through p (or value) can be arbitrarily interleaved with reads and writes to q" 具有明确定义的行为。

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

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

首先,转换指针不会导致任何别名冲突(尽管它可能会导致对齐冲突)。

别名是指通过与对象类型不同的左值读取或写入 对象的过程。

如果一个对象的类型为 T,而我们通过 X&Y& read/write 它,那么问题是:

  • 可以X别名T吗?
  • 可以Y别名T吗?

X 是否可以作为 Y 的别名,反之亦然,这并不直接重要,正如您在问题中所关注的那样。但是,如果 XY 完全不兼容,编译器可以推断出不存在可以同时被 XY 别名的类型 T,因此可以假定这两个引用引用不同的对象。

因此,要回答您的问题,这完全取决于 PopMessage 的作用。如果代码是这样的:

const char *PopMessage()
{
     static MessageJ foo = .....;
     return reinterpret_cast<const char *>(&foo);
}

那么写就可以了:

const char *ptr = PopMessage();
auto bar = reinterpret_cast<const MessageJ*>(foo);

auto baz = *bar;    // OK, accessing a `MessageJ` via glvalue of type `MessageJ`
auto ch = ptr[4];   // OK, accessing a `MessageJ` via glvalue of type `char`

等等。 const 与它无关。事实上,如果你在这里没有使用 const(或者你把它扔掉了),那么你也可以毫无问题地写通 barptr

另一方面,如果 PopMessage 是这样的:

const char *PopMessage()
{
    static char buf[200];
    return buf;
}

那么行 auto baz = *bar; 将导致 UB,因为 char 不能被 MessageJ 别名化。请注意,您可以使用 placement-new 来更改对象的动态类型(在这种情况下,据说 char buf[200] 已停止存在,而 placement-new 创建的新对象存在并且其类型为 T).

所以我的理解是你在做类似的事情:

enum MType { J,K };
struct MessageX { MType type; };

struct MessageJ {
    MType type{ J };
    int id{ 5 };
    //some other members
};
const char* popMessage() {
    return reinterpret_cast<char*>(new MessageJ());
}
void MessageServer(const char* foo) {
    const MessageX* msgx = reinterpret_cast<const MessageX*>(foo);
    switch (msgx->type) {
        case J: {
            const MessageJ* msgJ = reinterpret_cast<const MessageJ*>(foo);
            std::cout << msgJ->id << std::endl;
        }
    }
}

int main() {
    const char* foo = popMessage();
    MessageServer(foo);
}

如果这是正确的,那么表达式 msgJ->id 是正确的(对 foo 的任何访问也是如此),因为 msgJ 具有正确的动态类型。另一方面,msgx->type 确实会导致 UB,因为 msgx 具有不相关的类型。指向 MessageJ 的指针在两者之间被转换为 const char* 的事实完全无关紧要。

正如其他人所引用的,这里是标准中的相关部分(“glvalue”是取消引用指针的结果):

If a program attempts to access the stored value of an object through a glvalue of other than one of the following types the behavior is undefined:52

  1. the dynamic type of the object,
  2. a cv-qualified version of the dynamic type of the object,
  3. a type similar (as defined in 4.4) to the dynamic type of the object,
  4. a type that is the signed or unsigned type corresponding to the dynamic type of the object,
  5. a type that is the signed or unsigned type corresponding to a cv-qualified version of the dynamic type of the object,
  6. an aggregate or union type that includes one of the aforementioned types among its elements or nonstatic data members (including, recursively, an element or non-static data member of a subaggregate or contained union),
  7. a type that is a (possibly cv-qualified) base class type of the dynamic type of the object,
  8. a char or unsigned char type.

就“投射到 char*”与“从 char* 投射”的讨论而言:
您可能知道该标准并未讨论严格的别名本身,它仅提供了上面的列表。严格别名是一种基于该列表的分析技术,供编译器确定哪些指针可能相互别名。就优化而言,如果将指向 MessageJ 对象的指针转换为 char* 或相反,则没有区别。编译器不能(在没有进一步分析的情况下)假定 char*MessageX* 指向不同的对象,并且不会基于此执行任何优化(例如重新排序)。

当然这不会改变这样一个事实,即通过指向不同类型的指针访问 char 数组在 C++ 中仍然是 UB(我假设主要是由于对齐问题)并且编译器可能会执行其他优化,这可能毁了你的一天。

编辑:

What I'm asking is: Will the initialization of a const object, by casting from a char*, ever be optimized below where that object is cast to another type of object, such that I am casting from uninitialized data?

不,不会。别名分析不会影响指针本身的处理方式,但会影响通过该指针的访问。编译器不会将写入访问(将内存地址存储在指针变量中)与读取访问(复制到其他变量/加载地址以访问内存位置)重新排序到同一变量。