具有线程安全规则建议的非常量参数的复制构造函数?

alf*_*lfC 9 c++ legacy const-correctness thread-safety c++11

我有一段遗留代码的包装器。

class A{
   L* impl_; // the legacy object has to be in the heap, could be also unique_ptr
   A(A const&) = delete;
   L* duplicate(){L* ret; legacy_duplicate(impl_, &L); return ret;}
   ... // proper resource management here
};
Run Code Online (Sandbox Code Playgroud)

在这个遗留代码中,“复制”对象的函数不是线程安全的(当调用相同的第一个参数时),因此它没有const在包装器中标记。我想遵循现代规则:https : //herbsutter.com/2013/01/01/video-you-dont-know-const-and-mutable/

duplicate看起来是实现复制构造函数的好方法,除了它不是的细节const。因此我不能直接这样做:

class A{
   L* impl_; // the legacy object has to be in the heap
   A(A const& other) : L{other.duplicate()}{} // error calling a non-const function
   L* duplicate(){L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
Run Code Online (Sandbox Code Playgroud)

那么,如何摆脱这种矛盾的局面呢?

(让我们说这legacy_duplicate不是线程安全的,但我知道当对象退出时会使其保持原始状态。作为 C 函数,该行为仅被记录在案,但没有常量的概念。)

我能想到很多可能的场景:

(1)一种可能性是根本无法实现具有通常语义的复制构造函数。(是的,我可以移动对象,这不是我需要的。)

(2)另一方面,复制一个对象本质上是非线程安全的,因为复制一个简单类型可以找到处于半修改状态的源,所以我可以继续这样做,也许,

class A{
   L* impl_;
   A(A const& other) : L{const_cast<A&>(other).duplicate()}{} // error calling a non-const function
   L* duplicate(){L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
Run Code Online (Sandbox Code Playgroud)

(3)或者甚至只是声明duplicateconst 并在所有上下文中撒谎关于线程安全。(毕竟遗留函数并不关心const所以编译器甚至不会抱怨。)

class A{
   L* impl_;
   A(A const& other) : L{other.duplicate()}{}
   L* duplicate() const{L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
Run Code Online (Sandbox Code Playgroud)

(4)最后,我可以按照逻辑制作一个接受非常量参数的复制构造函数

class A{
   L* impl_;
   A(A const&) = delete;
   A(A& other) : L{other.duplicate()}{}
   L* duplicate(){L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
Run Code Online (Sandbox Code Playgroud)

事实证明,这适用于许多上下文,因为这些对象通常不 const.

问题是,这是一条有效的还是通用的路线?

我无法命名它们,但我直觉地认为在拥有非常量复制构造函数的道路上会出现很多问题。由于这种微妙之处,它可能不符合值类型的要求。

(5)最后,虽然这似乎有点矫枉过正,并且可能会产生很高的运行时成本,但我可以添加一个互斥锁:

class A{
   L* impl_;
   A(A const& other) : L{other.duplicate_locked()}{}
   L* duplicate(){
      L* ret; legacy_duplicate(impl_, &ret); return ret;
   }
   L* duplicate_locked() const{
      std::lock_guard<std::mutex> lk(mut);
      L* ret; legacy_duplicate(impl_, &ret); return ret;
   }
   mutable std::mutex mut;
};
Run Code Online (Sandbox Code Playgroud)

但是被迫这样做看起来很悲观,并使班级变大。我不确定。我目前倾向于(4)(5)或两者的组合。


编辑 1:

另外一个选项:

(6)忘记所有重复成员函数的无意义,只需legacy_duplicate从构造函数调用并声明复制构造函数不是线程安全的。(如有必要,请制作该类型的另一个线程安全版本,A_mt

class A{
   L* impl_;
   A(A const& other){legacy_duplicate(other.impl_, &impl_);}
};
Run Code Online (Sandbox Code Playgroud)

编辑2:

这可能是遗留函数功能的一个很好的模型。请注意,通过触摸输入,就第一个参数表示的值而言,调用不是线程安全的。

void legacy_duplicate(L* in, L** out){
   *out = new L{};
   char tmp = in[0];
   in[0] = tmp; 
   std::memcpy(*out, in, sizeof *in); return; 
}
Run Code Online (Sandbox Code Playgroud)

编辑3: 我最近了解到std::auto_ptr有一个非常量“复制”构造函数的类似问题。效果是auto_ptr不能在容器内使用。https://www.quantstart.com/articles/STL-Containers-and-Auto_ptrs-Why-They-Dont-Mix/

Mic*_*zyk 0

我只包含您的选项 (4) 和 (5),但当您认为对性能有必要时,明确选择线程不安全行为。

这是一个完整的例子。

#include <cstdlib>
#include <thread>

struct L {
  int val;
};

void legacy_duplicate(const L* in, L** out) {
  *out = new L{};
  std::memcpy(*out, in, sizeof *in);
  return;
}

class A {
 public:
  A(L* l) : impl_{l} {}
  A(A const& other) : impl_{other.duplicate_locked()} {}

  A copy_unsafe_for_multithreading() { return {duplicate()}; }

  L* impl_;

  L* duplicate() {
    printf("in duplicate\n");
    L* ret;
    legacy_duplicate(impl_, &ret);
    return ret;
  }
  L* duplicate_locked() const {
    std::lock_guard<std::mutex> lk(mut);
    printf("in duplicate_locked\n");
    L* ret;
    legacy_duplicate(impl_, &ret);
    return ret;
  }
  mutable std::mutex mut;
};

int main() {
  A a(new L{1});
  const A b(new L{2});

  A c = a;
  A d = b;

  A e = a.copy_unsafe_for_multithreading();
  A f = const_cast<A&>(b).copy_unsafe_for_multithreading();

  printf("\npointers:\na=%p\nb=%p\nc=%p\nc=%p\nd=%p\nf=%p\n\n", a.impl_,
     b.impl_, c.impl_, d.impl_, e.impl_, f.impl_);

  printf("vals:\na=%d\nb=%d\nc=%d\nc=%d\nd=%d\nf=%d\n", a.impl_->val,
     b.impl_->val, c.impl_->val, d.impl_->val, e.impl_->val, f.impl_->val);
}
Run Code Online (Sandbox Code Playgroud)

输出:

in duplicate_locked
in duplicate_locked
in duplicate
in duplicate

pointers:
a=0x7f85e8c01840
b=0x7f85e8c01850
c=0x7f85e8c01860
c=0x7f85e8c01870
d=0x7f85e8c01880
f=0x7f85e8c01890

vals:
a=1
b=2
c=1
c=2
d=1
f=2
Run Code Online (Sandbox Code Playgroud)

这遵循Google 风格指南,其中const传达线程安全性,但调用 API 的代码可以选择退出const_cast

  • 是的,如果您计划在线程安全和线程不安全上下文中使用“A”,则应该将“const_cast”拉到调用代码中,以便清楚地知道哪里违反了线程安全。在 API 背后添加额外的安全性(互斥体)是可以的,但隐藏不安全性(const_cast)是不行的。 (2认同)