std::function的Const正确性问题

创建时间: 2019年12月31日

const类型限定符(type qualifier)是C++语言设计的一大亮点。我们围绕着这个语言特性使用“const正确性” (const correctness)的实践来防止const对象遭到改变。“const正确性”的规则对大多数的类的实现都不难被遵守。但是对使用到类型擦除(type erasure)的类,“const正确性”更难被遵守。不幸的是,因为短见,C++标准库中的std::function类就成了一个“const正确性”不被遵守的例子。

问题根源

std::function的成员函数operator()被标记为const却可以改变内在的函数对象。比如说:

const std::function<int()> f {[x=0]() mutable { return ++x; }};
f(); // returns 1
f(); // returns 2

文档N43481第一次指出这个问题。并且直言

This violates not only basic tenets of const-correctness, but also the data race avoidance guarantees in [res.on.data.races]/p3, which states that "A C++ standard library function shall not directly or indirectly modify objects accessible by threads other than the current thread unless the objects are accessed directly or indirectly via the function's non-const arguments, including this".

大意是说operator()的语义不仅仅违法了“const正确性”基本原则,更破坏了标准库无数据争用(data race)的保证。

解决办法

类似于function的类需要争对const以及非const有分别的模板特化(template specializations):

template<class Sig> class function; // not defined

template<class R, class... Args>
  class function<R(Args...)>;

template<class R, class... Args>
  class function<R(Args...) const>;

const特化的operator()应该被标记为const,而它的构造函数不可以接受可变的函数对象。

function<int() const> f1 {[x=0]() { return x; }};
f1(); // 正常

function<int() const> f2 {[x=0]() mutable { return ++x; }}; // 无法编译

反过来说非const特化的operator()不会被标记为const,所以你不能调用const版本的function

function<int()> f1 {[x=0]() mutable { return ++x; }};
f1(); // 正常

const function<int()> f2 {[x=0]() mutable { return ++x; }};
f2(); // 无法编译

展望未来

我并不期望std::function会做出破坏向后兼容的更改。截至本文发表时,我把希望放在已被提议的std::unique_function2上。它不仅修复std::function中“const正确性”的bug,而且还带来了其他的改善。如果C++标准中有了std::function替代品,我们就可以弃用(deprecate)std::function如同当年的std::auto_ptr。与此同时,我们也可以自己实现unique_function我的Github就有一个库实现了这个功能。