使用const_cast和std::move从非引用类型中移除const属性

20

我有一个外部库,无法修改。该库声明了一个模板函数,由于某些原因返回非引用的const对象:

template<class C>
const C foo();

我有另一个无法修改的外部库。该库声明了一个不可复制的类,并仅从非const对象拥有移动构造函数:

struct bar {
    bar();
    bar(const bar&)=delete;
    bar(bar&&);
};

现在我需要使用foo<bar>。一个简单的用法:

bar buz() {
    return foo<bar>();
}

运行失败

main.cpp: In function 'bar buz()':
main.cpp:13:21: error: use of deleted function 'bar::bar(const bar&)'
     return foo<bar>();
                     ^
main.cpp:8:5: note: declared here
     bar(const bar&)=delete;
     ^~~

这很有道理,而且没有简单的解决方法可以使代码编译。

不过,如果我添加一些更复杂的解决方法:

bar buz() {
    return const_cast<bar&&>(std::move(foo<bar>()));
}

代码编译成功且整个代码按预期工作(不仅是上述简化示例,还包括我的真实代码)。

然而,这是否安全,或者我是否遇到了某些未定义的行为?是否有更好的解决方法?


我已经阅读并理解了关于从函数返回const的问题(1, 2),那里的常见答案似乎是在现代C++中不鼓励返回const对象,但我的问题不是这个,而是当外部库返回const对象时如何解决此问题。


const_cast<bar>(foo<bar>()) 不够吗? 这仍然会在返回语句中留下未命名对象,因此移动语义应该会启动。 - Hcorg
@Hcorg,错误:使用const_cast处理类型为“bar”的非指针、引用或数据成员指针类型无效 - Petr
2
@Petr:/很遗憾,我会尝试使用const_cast<bar&&>而不使用std::move,这样可以更简单。此外-您确定foo内部没有任何特定的内容使得“const”是合理的吗? - Hcorg
2
不要依赖未定义的行为。如果需要,切换库。 - David Haim
1
@Petr 嗯,我更喜欢能够保证正常工作的长代码,而不是可能在未来出现问题的短代码 :) - eerorika
显示剩余6条评论
3个回答

7
如果 bar 的移动构造函数有修改操作,那么放弃 const 将导致未定义的行为。您可以通过以下方式解决问题,而不会引入未定义的行为:
struct wrapped_bar {
    mutable bar wrapped;
};

bar buz()
{
    return foo<wrapped_bar>().wrapped;
}

拥有可变的 "wrapped" 成员意味着该成员是非常量的,即使作为整体的 "wrapped_bar" 对象是const的。根据 "foo()" 的工作方式,您可能需要向 "wrapped_bar" 添加成员以使其更像一个 "bar"。

+1 好主意!鉴于 wrapped_barbuz() 的实现细节,最好在 buz() 的函数体内定义它。当然,所有这些都依赖于 foo<T> 对于任何 T 都能正常工作。 - Walter

5
从技术上讲,您正在将程序暴露给未定义的行为。因为原始对象 C(一个临时对象)被声明为 const,所以强制类型转换和修改它是非法的,并且违反了标准。(我假设移动构造函数对被移动的对象进行了一些修改。)
话虽如此,在您的环境中它可能会工作,我也看不到更好的解决办法。

2
@DavidHaim,首先,它被创建为const。返回的临时对象是一个独立的对象,并且函数签名要求它是const。然后,可能基于OP自己的陈述(整个代码按预期工作)。 - SergeyA
1
纯属猜测。这里唯一明确的答案是说它“可能是未定义行为”,因为OP只关心去掉const的强制转换 - uh oh somebody needs a pupper
@sleeptightpupper,请问您能详细说明我在哪个点上进行了推测吗? - SergeyA
我不认为 const 强制转换会导致 UB,而是因为如果修改已声明为 const 的实际对象。 - David G
我认为这是正确的答案。我会为 foo<bar>() 创建一个包装器,并将其保留在单独的翻译单元中,这样在编译包装器的调用者时,编译器无法看到对象最初是常量并执行一些无用的操作。 - Martin Bonner supports Monica
显示剩余4条评论

1
由于函数调用的结果本身就是一个R-Value,因此在返回语句中不需要对其应用std::move - const_cast<bar&&>(foo<bar>())就足够了。这样可以使代码更易读一些。
然而 - 并没有标准保证这对所有bar类型都有效。更重要的是 - 在某些情况下,这可能会导致未定义的行为。(想象一下一些非常侵入性的优化,它完全消除了foo并将其结果作为内存中的“静态数据”段中的对象 - 就像foo是一个constexpr一样。然后调用移动构造函数,它可能修改其参数,可能会导致访问冲突异常)。
你所能做的就是要么切换到不同的库(或者如果可能的话,要求库维护者修复API),要么创建一些单元测试并将其包含在构建过程中 - 只要测试通过,你就应该没问题(记得使用与“生产”构建相同的优化设置 - const_cast是那些强烈依赖于编译设置的东西之一)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接