【问题标题】:Non-trivial example of undefined behavior with const_castconst_cast 未定义行为的重要示例
【发布时间】:2016-10-09 01:53:02
【问题描述】:

据我了解,以下代码是根据 c++ 标准(特别是第 7.1.5.1.4 [dcl.type.cv]/4 节)未定义的行为。

#include <iostream>

struct F;
F* g;

struct F {
    F() : val(5)
    {
        g = this;
    }
    int val;
};


const F f;

int main() {
    g->val = 8;
    std::cout << f.val << std::endl;
}

但是,我尝试过的每个编译器和优化设置都会打印“8”。

问题:这种类型的“隐式 const_cast”是否有会出现意外结果的示例?

我希望有一些像结果一样壮观的东西

#include <iostream>
int main() {
    for (int i = 0; i <=4; ++i)
        std::cout << i * 1000000000 << std::endl;
}

开启,例如,带有 -O2 的 gcc 4.8.5

EDIT:标准中的相关部分

7.1.5.1.4:除了可以修改任何声明为 mutable (7.1.1) 的类成员外,任何在 const 对象的生命周期内修改它的尝试 (3.8) 导致未定义的行为。

回复建议重复的评论;它不是重复的,因为我要求的是出现“意外”结果的示例。

【问题讨论】:

  • 这是一个非常狡猾的设置 - 非常好。
  • 我确实喜欢底部那个 sn-p 的症状。零,一,二,放开海妖!
  • 尝试定义未定义行为的价值是什么?
  • @Kaz 不幸的是,破坏以前有效代码的“修复”违反了 C++ 的原则。即便如此,即使按照今天的标准,C++ 允许这种“黑客攻击”这一事实也是该语言的卖点之一,因为它允许一些人按时交付他们的项目,无论你最后遇到什么问题分钟,即使以可维护性为代价。不错的题外话,不过

标签: c++ undefined-behavior


【解决方案1】:

没有那么壮观:

f.h(省略守卫):

struct F;
extern F* g;

struct F {
    F() : val(5)
    {
        g = this;
    }
    int val;
};

extern const F f;
void h();

TU1:

#include "f.h"
// definitions
F* g;
const F f;
void h() {}    

TU2:

#include "f.h"
#include <iostream>
int main() {
    h(); // ensure that globals have been initialized
    int val = f.val;
    g->val = 8;
    std::cout << (f.val == val) << '\n';
}

使用g++ -O2 编译时打印1,使用-O0 编译时打印0

【讨论】:

    【解决方案2】:

    “未定义”行为的主要情况通常是,如果有人看到const,他们会假设它没有改变。所以,const_cast 故意做了很多库和程序不希望做的事情,或者认为是明确的未定义行为。重要的是要记住,并非所有未定义的行为都来自标准,即使这是该术语的典型用法。

    也就是说,我能够在标准库中找到一个地方,可以将这种想法应用于做一些我认为更狭义地被认为是未定义行为的事情:使用“重复键”生成 std::map

    #include "iostream"
    #include "map"
    
    int main( )
    {
        std::map< int, int > aMap;
    
        aMap[ 10 ] = 1;
        aMap[ 20 ] = 2;
    
        *const_cast< int* >( &aMap.find( 10 )->first ) = 20;
    
        std::cout << "Iteration:" << std::endl;
        for( std::map< int,int >::iterator i = aMap.begin(); i != aMap.end(); ++i )
            std::cout << i->first << " : " << i->second << std::endl;
    
        std::cout << std::endl << "Subscript Access:" << std::endl;
        std::cout << "aMap[ 10 ]" << " : " << aMap[ 10 ] << std::endl;
        std::cout << "aMap[ 20 ]" << " : " << aMap[ 20 ] << std::endl;
    
        std::cout << std::endl << "Iteration:" << std::endl;
        for( std::map< int,int >::iterator i = aMap.begin(); i != aMap.end(); ++i )
            std::cout << i->first << " : " << i->second << std::endl;
    }
    

    输出是:

    Iteration:
    20 : 1
    20 : 2
    
    Subscript Access:
    aMap[ 10 ] : 0
    aMap[ 20 ] : 1
    
    Iteration:
    10 : 0
    20 : 1
    20 : 2
    

    使用g++.exe (Rev5, Built by MSYS2 project) 5.3.0 构建。

    显然,访问密钥和存储对中的密钥值不匹配。似乎除非通过迭代,否则无法访问 20:2 对。

    我猜这是因为map 被实现为树。更改值会将其保留在最初的位置(10 将去的位置),因此它不会覆盖另一个 20 键。同时,添加一个实际的10 并不会覆盖旧的10,因为在检查键值时,它实际上并不相同

    我现在没有标准可以查看,但我认为这在某些层面上违反了map 的定义。

    这也可能导致更糟糕的行为,但使用我的编译器/操作系统组合,我无法让它做任何更极端的事情,比如崩溃。

    【讨论】:

    • @T.C.不能真正引用未定义的行为,但是我在这一点上是错误的。我进行了更深入的调查,并将其添加为编辑。真实的情况是陌生的。
    • 什么是未定义行为?
    • 如果你弄乱了一个类的内部结构,违反了它的契约并扰乱了它的不变量,那么不管在这个过程中是否使用const_cast,它当然会崩溃。
    • @T.C.正是这一点,只是使用标准定义的功能执行此操作会变成未定义的行为。
    • @nwp 我在那句话的后半部分解释了它。它可以让您轻松打破假设您将遵守正常约定的事情。从标准说它是未定义/没有定义它的意义上说,它不是完全 未定义的行为,但它几乎总是 在单个程序/库的意义上使用它非常容易损坏。但是就是说,当您按照标准进行操作时,我认为这完全是“未定义的行为”。您正在做的事情标准没有用它提供的东西来定义。显然,它没有得到妥善处理。
    猜你喜欢
    • 2011-11-13
    • 2011-08-23
    • 2021-11-15
    • 1970-01-01
    • 1970-01-01
    • 2019-05-04
    • 1970-01-01
    相关资源
    最近更新 更多