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

时间:2015-03-18 11:45:14

标签: c++ const alias reinterpret-cast strict-aliasing

我的公司使用消息服务器将消息发送到const char*,然后将其转换为消息类型。

在询问this question之后,我对此感到担忧。我不知道消息服务器中有任何不良行为。 const变量是否可能不会出现别名问题?

例如,假设foo是以MessageServer中的一种方式定义的:

  1. 作为参数:void MessageServer(const char* foo)
  2. MessageServer顶部的const变量:const char* foo = PopMessage();
  3. 现在MessageServer是一个巨大的功能,但它永远不会向foo分配任何内容,但在MessageServer的逻辑foo 中的1点将被转换为选定的消息类型。

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

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

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

    修改

    Jarod42's answer发现从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);
    

    我对Jarod42's answer的理解是,temp投射使其合法化。

    修改

    我对序列化,对齐,网络传递等方面有一些评论。这不是这个问题的关键。

    这是关于strict aliasing的问题。

      

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

    我要问的是: const对象的初始化(通过从char*进行转换)是否会在该对象转换为其他类型对象的位置进行优化,这样我就是从未初始化的数据中投射出来的?

5 个答案:

答案 0 :(得分:3)

首先,转换指针不会导致任何锯齿违规(尽管它可能会导致对齐违规)。

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

如果对象的类型为T,我们通过X&Y&读取/写入,则问题为:

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

X是否可以别名Y或者反之亦然,因为您似乎专注于您的问题。但是,编译器可以推断XY是否完全不兼容,因为TX之间没有这样的类型Y,因此,它可以假设两个引用引用不同的对象。

所以,要回答你的问题,这一切都取决于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创建的新对象存在且其类型为{{1 }})。

答案 1 :(得分:2)

  

我的公司使用消息服务器,将消息发送到const char * ,然后将其转换为消息类型。

只要你的意思是它做了reinterpret_cast(或者是一个转换为reinterpret_cast的C风格的演员):

MessageJ *j = new MessageJ();

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

以后再使用相同的指针并将其重新解释为将其重新设置为实际的基础类型,然后该过程完全合法:

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指向的基础对象实际上是 a MessageJ时,上述内容是合法的,否则它是未定义的行为。 reinterpret_cast到char*并再次返回产生原始类型指针。实际上,您可以将reprepret_cast重新解释为任何类型的指针,然后再返回并获取原始类型指针。来自this reference

  

使用reinterpret_cast只能进行以下转换......

     

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

     

键入别名

     

当对类型为T1的对象的指针或引用是reinterpret_cast(或C样式强制转换)为指针或对不同类型T2的对象的引用时,强制转换总是成功,但只能访问生成的指针或引用如果T1和T2都是标准布局类型,则以下之一为真:

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

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

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

  

这里是否存在别名问题,或者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;
}

在此示例中,由于严格的别名规则,编译器可以在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

在您的第一个示例中,fooconst char *bar是来自const MessageJ *的{​​{1}} reinterpret_cast。您进一步规定对象的基础类型实际上是foo,并且没有通过MessageJ进行读取。相反,它只被转换为const char *,然后只从中进行读取。由于您不读取或写入const MessageJ *别名,因此首先通过第二个别名访问时不会出现别名优化问题。这是因为通过不相关类型的别名不会对底层内存执行潜在的冲突操作。但是,即使您通过const char *进行了阅读,也可能仍然没有潜在的问题,因为类型别名规则允许此类访问(请参阅下文)以及通过foo或{{ 1}}会产生相同的结果,因为这里没有写入。

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

  

键入别名

     

当对类型为T1的对象的指针或引用是reinterpret_cast(或C样式强制转换)为指针或对不同类型T2的对象的引用时,强制转换总是成功,但只能访问生成的指针或引用如果T1和T2都是标准布局类型,并且满足以下条件之一:...

     
      
  • T2是char或unsigned char
  •   

foo引用(或指针)正在运行时,通过不相关类型的引用(或指针)操作所允许的严格别名优化明确禁止。编译器必须是偏执的,通过char*引用(或指针)的操作可以影响并受到通过其他引用(或指针)完成的操作的影响。在对charchar进行读写操作的修改示例中,您仍然可以定义行为,因为foobar。因此,不允许编译器优化以重新排序或消除两个别名上的操作,其方式与写入的代码的串行执行冲突。同样,它也不得不重新加载可能通过别名操作影响的值。

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

These两个technical references (3.10.10)可用于回答您的问题。 These其他references帮助更好地了解技术信息。

====
编辑:在下面的评论中,zmb对象虽然char*可以合法地为不同类型设置别名,但反过来却不正确,因为有几个来源似乎以不同的形式说:{{{ 1}}严格别名规则的例外是非对称的,单向&#34;规则。

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

char*

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

  

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

     
      
  • 对象的动态类型......

  •   
  • char或unsigned char类型......

  •   
     

^ 52:此列表的目的是指定对象可能或可能没有别名的情况。

在上面的程序中,我通过其实际类型和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; } 实际上是否为foo别名(例如 - 想象foo在图书馆中定义。也许程序可以通过检查指针本身的值或咨询RTTI来在运行时检测到这种别名,但这会产生的开销不值得。相反,通常编译x并允许在yfoo碰巧彼此别名时定义的行为的更好方法是始终假设它们可以(即 - 禁用严格的别名优化)当foo正在播放时。

这是我编译并运行上述程序时会发生什么:

x

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

让我们检查一些seemingly conflicting sources

  

相反的情况并非如此。将char *转换为除char *之外的任何类型的指针并取消引用它通常都是严格别名规则的声音。换句话说,通过char *从一个类型的指针转​​换为一个不相关类型的指针是未定义的

粗体位是为什么这个引用不适用于我的答案所解决的问题,也不适用于我刚刚给出的例子。在我的回答和示例中,通过y和对象本身的实际类型来访问别名内存,这可以是定义的行为。

  

C和C ++都允许通过char *(或者特别是char类型的左值)访问任何对象类型。他们不允许通过任意类型访问 char对象。所以,是的,规则是一种方式&#34; 。规则&#34;

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

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

char
  

p和p都指向相同的地址,它们是同一个内存的别名。该语言的作用是提供一组规则来定义保证的行为:通过q读取写入精细,其他方式不正确

标准和许多例子清楚地表明&#34;通过q写入,然后读取p(或值)&#34;可以很好地定义行为。什么不是很清楚,但我在这里争论的是&#34;通过p(或值)写,然后读q&#34;是总是定义良好。我进一步声称,通过p(或值)读取和写入可以任意地与读取和写入q&#34;行为明确。

现在对前面的陈述有一个警告,为什么我一直在撒上“#34; can&#34;在整个上述文本中。如果您有一个类型char*引用和一个char引用,它将别名相同的内存,那么int value; int *p = &value; char *q = reinterpret_cast<char*>(&value); 引用上的读取和写入与T引用上的读取任意交错是始终定义明确。例如,您可以通过char引用多次修改底层内存的十六进制转储来重复打印它。该标准保证严格的别名优化不会应用于这些交错访问,否则可能会给你未定义的行为。

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

答案 2 :(得分:2)

所以我的理解是你正在做这样的事情:

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”是取消引用指针的结果):

  

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

     
      
  1. 对象的动态类型,
  2.   
  3. 对象的动态类型的cv限定版本
  4.   
  5. 与对象的动态类型相似的类型(如4.4中所定义)
  6.   
  7. 与对象的动态类型对应的有符号或无符号类型的类型
  8.   
  9. 与对象的动态类型的cv限定版本对应的有符号或无符号类型的类型,
  10.   
  11. 聚合或联合类型,包括其元素或非静态数据成员中的上述类型之一(包括递归地,子聚合或包含联合的元素或非静态数据成员),
  12.   
  13. 一种类型,它是对象动态类型的(可能是cv限定的)基类类型,
  14.   
  15. char或unsigned char类型。
  16.   

关于“施放到char*”与“从char*施放”的讨论涉及:
你可能知道标准没有谈论严格的别名,它只提供上面的列表。严格别名是一种基于该列表的分析技术,用于编译器确定哪些指针可能相互混淆。就优化而言,如果指向MessageJ对象的指针被强制转换为char*,反之亦然,那么它就没有区别。编译器不能(没有进一步分析)假设char*MessageX*指向不同的对象,并且不会基于此执行任何优化(例如重新排序)。

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

编辑:

  

我要问的是:是否会初始化const对象   从char *转换,在该对象的下方进行优化   转换为另一种类型的对象,这样我就是从中投射出来的   未初始化的数据?

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

答案 3 :(得分:0)

使用(constchar*类型时没有别名问题,请参阅最后一点:

  

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

     
      
  • 对象的动态类型,
  •   
  • 对象的动态类型的cv限定版本,
  •   
  • 与对象的动态类型相似的类型(如4.4中所定义)
  •   
  • 与对象的动态类型对应的有符号或无符号类型的类型
  •   
  • 与对象的动态类型的cv限定版本对应的有符号或无符号类型的类型,
  •   
  • 聚合或联合类型,包括-its元素或非静态数据成员中的上述类型之一(包括递归地,子聚合或包含联合的元素或非静态数据成员),
  •   
  • 一种类型,它是对象动态类型的(可能是cv限定的)基类类型,
  •   
  • char或unsigned char类型。
  •   

答案 4 :(得分:0)

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

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

您不会在x86 / AMD64架构上遇到任何问题,因为它们允许未对齐访问。但是,有一天你可能会发现你正在开发的代码被移植到移动ARM架构中,然后未对齐的访问就成了问题。

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