【问题标题】:Modifying const object through pointer obtained during construction通过构造时获取的指针修改const对象
【发布时间】:2016-10-27 20:56:27
【问题描述】:

我刚刚发现在没有任何const_cast 魔法的情况下修改 const 对象是多么容易。考虑:

#include <iostream>

class Test {
public:
    Test(int v)
        :m_val{ v },
        m_ptr{ &m_val }
    {}

    int get() const { return m_val; }
    void set(int v) const { *m_ptr = v; }

private:
    int m_val;
    int* m_ptr;
};

int main()
{
    const Test t{ 10 };

    std::cout << t.get() << '\n';
    t.set(0);
    std::cout << t.get() << '\n';

    return 0;
}

Clang、GCC 和 MSVC 的最新版本不显示任何警告并产生预期的输出:

10 0

这是根据当前标准明确定义的行为吗?如果它是未定义的,如果 m_valstd::aligned_storage_t&lt;sizeof(int), alignof(int)&gt; 类型和构造函数 new'ed int 呢?我相信在小缓冲区优化方面这是很常见的情况。

编辑

谢谢,看来这只是另一种自爆的方式。 令人不安的是:

struct Test2 {
    int i;
    void operator()() { ++i; }
};

const std::function<void()> f{ Test2{ 10 } };
f();

当实现选择将Test2 对象存储在f 中时也是未定义的行为(在libc++ 和Visual Studio 中就是这种情况)

【问题讨论】:

  • 我认为是UB修改了m_val
  • "7.1.5.1/4:除了可以修改任何声明为可变的类成员(7.1.1)外,任何在其生命周期(3.8)期间修改 const 对象的尝试都会导致未定义行为。”
  • 对于constexpr,编译器发现了错误:Demo。 :-)

标签: c++ constants


【解决方案1】:

const 强制执行“按位常量”,但您通常想要的是“逻辑常量”。

对于包含指针的对象,这意味着 const 成员函数不能修改指针本身,但可以修改指针所指的内容。换句话说,这些示例格式正确,但行为未定义。

要获得逻辑常量,您 1) 使用mutable(或有时const_cast)来允许修改不影响对象逻辑状态的成员(例如,缓存值/记忆),并且 2) 通常具有手动强制不通过指针写入数据(但如果它是拥有指针,则该所有权可能应该委托给 only 管理该数据所有权的对象,在这种情况下,通常应该将其设为 const防止写入它拥有的数据)。

至于有一个非常量指针指向的数据本身可能已经被 const 修改的具体细节,好吧,你基本上只是得到一个与const_cast 大致相同的东西的(持久)版本通常用于:获取对数据的非常量访问,否则您将只有一个 const 指针。您可以确保仅以不会导致问题的方式使用它(但仅拥有和/或写入该指针本身并不一定会导致问题)。

换句话说,我们这里有两个指向某些数据的独立指针。 this 允许您访问对象的数据。在const 成员函数中,您只能通过this 读取(不能)写入数据,除非(如上所述)它被标记为mutable。在这种情况下,您正在保存指向相同数据的第二个指针。由于没有任何东西可以将其标记为指向 const 的指针,因此不是,因此您可以非常量地访问它指向的数据。

【讨论】:

  • 请注意,m_val(属于 const 实例 t)已被修改。
  • 这就是为什么我们需要propagate_const
  • 那么,问题中的示例是否合法?
  • @MikeMB:这两个示例都具有未定义的行为。
  • @MikeMB:我想从我的角度来看,有趣的问题主要是:“我应该如何编写可能必须处理这种情况的代码”,但我已经添加了一个明确的声明那些关心它的人。
【解决方案2】:

正如其他人在 cmets 中指出的那样:您正在修改 m_ptr 指向的对象。这个“指向”对象不是class Test 的一部分(就编译器而言)。这就是编译器允许您这样做的原因。

话虽如此,我相信这将是未定义的行为。那是因为m_ptr实际上指向了对象const Test t的另一个成员变量(m_val)!允许编译器进行 arggresively 优化,他们可能依赖 constness 来这样做。

唯一的例外是您使用 mutable 关键字,但这是另一回事。

【讨论】:

  • 请注意m_val(常量实例t)已修改。
  • 是的,我刚刚编辑了我的答案以反映这一点。感谢您指出这一点:)。
【解决方案3】:

C++中基本上有两种常量:物理常量和逻辑常量。

至于物理常量,在所考虑的代码段中一切都是完全有效的,因为set() 修改了由m_ptr 指向的值,而不是作为类的一部分的指针本身。

这里违反了逻辑常数。但是在 C++ 中有很多方法可以违反逻辑常量,因为这种常量很大程度上依赖于特定的类设计。

在上面的例子中,程序导致了 UB,因为它试图改变一个 const 对象。

从 n4296,7.1.6.1 cv-qualifiers

除了可以修改任何声明为 mutable (7.1.1) 的类成员, 任何在 const 对象的生命周期 (3.8) 期间修改其结果的尝试 在未定义的行为中。

【讨论】:

  • @NathanOliver 似乎编译器不知道它:)
【解决方案4】:

这是未定义的行为。并非所有const 声明的类型确实是常量,因此修改以这种方式声明的内容并不总是未定义的行为。您可以引用引用非 const 非 const 值的 const 类型,抛弃 const 并修改该值而不调用未定义的行为。在这种情况下,虽然原始定义是 const,所以您必须假设它是一个常量。

修改任何常量都是未定义的行为,是的,有很多方法可以“意外”这样做。在aligned_storage 版本中,是的,通过使用placement new 来修改该常量 数据是未定义的行为。

【讨论】:

  • placement-new 创建一个新对象,它不会修改现有对象。 (但有一条规则,在有/曾经有const 对象的地方使用placement-new 会导致未定义的行为)
猜你喜欢
  • 1970-01-01
  • 2011-01-31
  • 2014-05-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多