c++ unique_ptr用法 - 我如何将unique_ptr参数传递给构造函数或函数?




unique_ptr函数参数 unique_ptr作为参数 (5)

我是新移动C ++ 11中的语义,我不知道如何处理构造函数或函数中的unique_ptr参数。 考虑这个引用自身的类:

#include <memory>

class Base
{
  public:

    typedef unique_ptr<Base> UPtr;

    Base(){}
    Base(Base::UPtr n):next(std::move(n)){}

    virtual ~Base(){}

    void setNext(Base::UPtr n)
    {
      next = std::move(n);
    }

  protected :

    Base::UPtr next;

};

这是我应该如何写函数采取unique_ptr参数?

我需要在调用代码中使用std::move吗?

Base::UPtr b1;
Base::UPtr b2(new Base());

b1->setNext(b2); //should I write b1->setNext(std::move(b2)); instead?

Answers

编辑:这个答案是错误的,即使严格来说,代码的作品。 我只是把它留在这里,因为它下面的讨论太有用了。 这另一个答案是我最后编辑这个时候给出的最佳答案: 我如何将unique_ptr参数传递给构造函数或函数?

::std::move的基本思想是,传递给你unique_ptr应该使用它来表达他们知道他们传入的unique_ptr会失去所有权的知识。

这意味着你应该在方法中使用右值引用unique_ptr ,而不是unique_ptr本身。 这将无法正常工作,因为传入一个普通的旧unique_ptr需要复制,并且在unique_ptr的接口中明确禁止。 有趣的是,使用一个名为rvalue的引用将它重新转换为一个左值,所以你需要你的方法中使用::std::move

这意味着你的两个方法应该是这样的:

Base(Base::UPtr &&n) : next(::std::move(n)) {} // Spaces for readability

void setNext(Base::UPtr &&n) { next = ::std::move(n); }

然后使用这些方法的人会这样做:

Base::UPtr objptr{ new Base; }
Base::UPtr objptr2{ new Base; }
Base fred(::std::move(objptr)); // objptr now loses ownership
fred.setNext(::std::move(objptr2)); // objptr2 now loses ownership

正如你所看到的, ::std::move表示指针将在它最相关和有帮助的地方失去所有权。 如果这种情况无形中发生,那么使用你的课程的人objptr突然失去所有权,这是非常令人困惑的。


让我试着说明将指针传递给其内存由std::unique_ptr类模板的实例管理的对象的不同可行模式; 它也适用于较旧的std::auto_ptr类模板(我相信它允许所有使用该唯一指针,但对于这些模板,除了可修改的左值,在需要rvalues的地方将被接受,而不必调用std::move ),并在一定程度上也为std::shared_ptr

作为讨论的具体例子,我将考虑以下简单的列表类型

struct node;
typedef std::unique_ptr<node> list;
struct node { int entry; list next; }

这种列表的实例(不能被允许与其他实例共享部分或者是循环的)完全由持有初始list指针的人拥有。 如果客户端代码知道它存储的列表永远不会为空,那么它也可能选择直接存储第一个node而不是list 。 不需要定义node的析构函数:由于自动调用其字段的析构函数,所以一旦初始指针或节点的生存期结束,整个列表将由智能指针析构函数递归删除。

这种递归类型提供了一些机会来讨论在智能指针指向普通数据的情况下不太明显的情况。 此外,函数本身也偶尔提供(递归)客户端代码的示例。 typedef list当然偏向unique_ptr ,但是定义可以改为使用auto_ptrshared_ptr而不需要更改以下所述(尤其是关于不需要编写析构函数的异常安全性)。

传递智能指针的模式

模式0:传递指针或引用参数而不是智能指针

如果你的功能不关心所有权,这是首选方法:不要让它变成一个聪明的指针。 在这种情况下,您的函数不需要担心拥有指向的对象是 ,或者通过什么方式管理所有权,因此传递原始指针是非常安全的,也是最灵活的形式,因为不管所有权如何,客户端总是可以产生一个原始指针(通过调用get方法或从操作符地址)。

例如,计算这种列表长度的函数不应该给出list参数,而应该是一个原始指针:

size_t length(const node* p)
{ size_t l=0; for ( ; p!=nullptr; p=p->next.get()) ++l; return l; }

一个持有变量list head客户端可以调用这个函数作为length(head.get()) ,而选择代替非空列表的node n的客户端可以调用length(&n)

如果指针保证是非空的(这里不是这种情况,因为列表可能是空的),人们可能更喜欢传递引用而不是指针。 如果函数需要更新节点的内容,而不添加或删除任何节点(后者将涉及所有权),则它可能是指向非常量的指针/引用。

属于模式0类别的一个有趣案例是制作(深)副本的列表; 而一个这样做的函数当然必须转移它创建的副本的所有权,它不关心它正在复制的列表的所有权。 所以可以定义如下:

list copy(const node* p)
{ return list( p==nullptr ? nullptr : new node{p->entry,copy(p->next.get())} ); }

这个代码值得仔细看看,对于它为什么编译它的问题(在初始化列表中copy的递归调用的结果绑定到unique_ptr<node> ,也就是list的移动构造函数中的右值引用参数,当初始化生成的nodenext字段时),以及关于为什么它是异常安全的问题(如果在递归分配过程中内存用完并且某些new std::bad_alloc调用被调用,那么当时一个指针到部分构造的列表被匿名保存在为初始化列表创建的类型list中,并且其析构函数将清除该部分列表)。 顺便说一句,我们应该抵制用p代替第二个nullptr (正如我最初所做的那样)的诱惑,毕竟在这一点上已知它是空的:我们不能构造一个从(原始)指针到一个常量的智能指针,甚至当它被认为是空的时候。

模式1:按值传递智能指针

一个将智能指针值作为参数的函数拥有了马上指向的对象:调用者持有的智能指针(无论是在命名变量还是匿名临时变量中)都被复制到函数入口的参数值中,调用者的指针已经变为空(在临时情况下,复制可能已被消除,但在任何情况下,调用者都无法访问指向对象的指针)。 我想用现金称呼这种模式的呼叫 :呼叫者为呼叫的服务付款,并且可以在呼叫之后对所有权不抱任何幻想。 为了清楚std::move如果智能指针保存在变量中(技术上,如果参数是左值),语言规则要求调用者将参数包装在std::move ; 在这种情况下(但不适用于下面的模式3),这个函数完成它的名字,即将值从变量移动到一个临时变量,留下变量null。

对于被调用函数无条件地取得指向对象的所有权的情况,这种与std::unique_ptrstd::auto_ptr一起使用的模式是将指针与其所有权一起传递的一种好方法,这样可以避免内存泄漏。 尽管如此,我认为只有极少数情况下,模式3不会比模式1更受欢迎(因此略微偏重)。因此,我将不提供此模式的使用示例。 (但请参阅下面模式3的reversed示例,其中表明模式1至少可以做到这一点)。如果函数接受的参数多于此指针,则可能会出现另外的技术原因模式1 (使用std::unique_ptrstd::auto_ptr ):由于std::move(p)表达式传递指针变量p会发生实际的移动操作,因此不能假设p保留有用的值评估其他论点(评估顺序未指定),这可能导致微妙的错误; 相反,使用模式3确保在函数调用之前不会从p移动,所以其他参数可以通过p安全地访问一个值。

当与std::shared_ptr ,这种模式很有趣,因为使用单个函数定义时,调用者可以选择是否为自己保留指针的共享副本,同时创建要由函数使用的新共享副本(this当提供一个左值参数时会发生;在调用时使用的共享指针的拷贝构造函数会增加引用计数),或者只给函数一个指针副本而不保留一个或触及引用计数(当右值参数被提供,可能是一个包含在std::move调用中的左值)。 例如

void f(std::shared_ptr<X> x) // call by shared cash
{ container.insert(std::move(x)); } // store shared pointer in container

void client()
{ std::shared_ptr<X> p = std::make_shared<X>(args);
  f(p); // lvalue argument; store pointer in container but keep a copy
  f(std::make_shared<X>(args)); // prvalue argument; fresh pointer is just stored away
  f(std::move(p)); // xvalue argument; p is transferred to container and left null
}

同样可以通过单独定义void f(const std::shared_ptr<X>& x) (对于左值情况)和void f(std::shared_ptr<X>&& x) (对于右值情况)来实现,函数体的区别仅在于第一个版本调用复制语义(使用x时使用复制构造/赋值),而第二个版本移动语义(而不是像示例代码中那样编写std::move(x) )。 所以对于共享指针,模式1对于避免一些代码重复是有用的。

模式2:通过(可修改)左值引用传递智能指针

这里该函数只需要对智能指针有一个可修改的引用,但并不指示它将如何处理它。 我想打电话给这种方法:通过发出信用卡号码来确保付款。 该引用用于获取指向对象的所有权,但不一定要这样做。 这种模式需要提供一个可修改的左值参数,对应于该函数的期望效果可能包括在参数变量中留下有用值的事实。 具有rvalue表达式的调用者希望传递给这样的函数将被迫将其存储在命名变量中以便能够进行调用,因为语言仅提供对常量左值引用的隐式转换(指的是临时)从右值。 (与std::move处理相反的情况不同,从Y&&Y&类型转换是不可能的;但如果真的需要,可以通过简单的模板函数获得此转换;请参见https://.com/a/24868376/1436796 )。 对于被调用函数意图无条件地取得对象所有权的情况,从参数中窃取,提供左值参数的义务是给出错误的信号:在调用之后变量将没有有用的值。 因此模式3在我们的功能中给出了相同的可能性,但要求呼叫者提供右值,因此应该优选这种使用。

然而,模式2有一个有效的用例,即可以修改指针的函数,或以涉及所有权的方式指向的对象。 例如,一个将节点加入list前缀的函数提供了这样一个使用的例子:

void prepend (int x, list& l) { l = list( new node{ x, std::move(l)} ); }

很明显,在这里强制调用者使用std::move是不可取的,因为他们的智能指针在调用之后仍然拥有一个定义良好的非空列表,虽然与以前不同。

再次观察一下,如果由于缺少可用内存而导致prepend调用失败,会发生什么情况。 然后new调用会抛出std::bad_alloc ; 在这个时间点上,由于没有node可以被分配,所以可以肯定的是,来自std::move(l)右值引用(模式3)还不能被窃取,就像构建next字段那样未能分配的node 。 所以当错误被抛出时,原来的智能指针l仍然保持原始列表; 该列表将被智能指针析构函数正确销毁,或者如果由于足够早的catch子句而使得l仍然存在,它仍然会保留原始列表。

这是一个建设性的例子; 对这个问题的一个眨眼,人们也可以给出更具破坏性的例子,去除包含给定值的第一个节点(如果有的话):

void remove_first(int x, list& l)
{ list* p = &l;
  while ((*p).get()!=nullptr and (*p)->entry!=x)
    p = &(*p)->next;
  if ((*p).get()!=nullptr)
    (*p).reset((*p)->next.release()); // or equivalent: *p = std::move((*p)->next); 
}

这里的正确性也很微妙。 值得注意的是,在最后的声明中, reset 之前 (隐式地)销毁该节点(当它销毁的时候), 要被移除的节点内部的指针(*p)->nextrelease (通过release ,其返回指针但使原始空值) p )保存的旧值,确保当时只有一个节点被销毁。 (在注释中提到的替代形式中,这个时间将留给std::unique_ptr实例list的移动赋值运算符的内部实现;标准说20.7.1.2.3; 2该运算符应该行为“就好像通过调用reset(u.release()) ,因此这里的时间安全也应该是安全的。)

请注意, prependremove_first不能由存储总是非空列表的本地node变量的客户端调用,并且正确,因为给定的实现无法在这种情况下工作。

模式3:通过(可修改)右值引用传递智能指针

当简单地获取指针的所有权时,这是首选模式。 我想通过检查来调用这个方法:调用者必须接受放弃所有权,就好像提供现金一样,通过签署支票,但实际的提款被推迟到被调用的函数实际上窃取指针为止(与使用模式2时一样)。 “签名检查”具体意味着调用者必须在std::move包含一个参数(如模式1),如果它是一个左值(如果它是一个右值,“放弃所有权”部分是显而易见的,并且不需要单独的代码)。

请注意,技术上模式3的行为与模式2完全相同,因此被调用函数不必承担所有权; 但是我坚持认为,如果对所有权转让有任何不确定性(正常使用情况下),模式2应该优先于模式3,以便使用模式3隐含地向呼叫者表明他们放弃所有权。 有人可能会反驳说,只有通过模式1的参数传递真正的信号才会迫使所有者丧失所有权。 但如果客户对被调用函数的意图有任何疑问,她应该知道被调用函数的规范,这应该消除任何疑问。

令人惊讶的是,难以找到一个典型的例子,涉及使用模式3参数传递的list类型。 将列表b移动到另一个列表a的末尾是一个典型示例; 然而,使用模式2可以更好地通过(保留并保留操作结果):

void append (list& a, list&& b)
{ list* p=&a;
  while ((*p).get()!=nullptr) // find end of list a
    p=&(*p)->next;
  *p = std::move(b); // attach b; the variable b relinquishes ownership here
}

模式3参数传递的一个纯粹示例如下,它取得一个列表(及其所有权),并以相反的顺序返回包含相同节点的列表。

list reversed (list&& l) noexcept // pilfering reversal of list
{ list p(l.release()); // move list into temporary for traversal
  list result(nullptr);
  while (p.get()!=nullptr)
  { // permute: result --> p->next --> p --> (cycle to result)
    result.swap(p->next);
    result.swap(p);
  }
  return result;
}

这个函数可能被调用,如l = reversed(std::move(l)); 将列表反转成其本身,但反转列表也可以不同地使用。

在这里,参数会立即转移到局部变量以提高效率(可以直接使用参数l代替p ,但每次访问都会涉及额外的间接级别); 因此与模式1参数传递的差别很小。 事实上,使用这种模式,参数可以直接作为局部变量,从而避免了最初的移动; 这只是一个普遍原理的实例,如果通过引用传递的参数仅用于初始化局部变量,则可以通过值来传递它,并使用该参数作为局部变量。

标准提倡使用模式3,正如所见证的,所有提供的库函数都使用模式3来传输智能指针的所有权。特别有说服力的例子是构造函数std::shared_ptr<T>(auto_ptr<T>&& p) 。 该构造函数使用(在std::tr1 )获取可修改的左值引用(就像auto_ptr<T>& copy构造函数),因此可以使用auto_ptr<T> lvalue p来调用,如std::shared_ptr<T> q(p) ,之后p已被重置为空。 由于在参数传递中从模式2改变为3,这个旧代码现在必须重写为std::shared_ptr<T> q(std::move(p)) ,然后继续工作。 我明白,委员会不喜欢这里的模式2,但他们可以通过定义std::shared_ptr<T>(auto_ptr<T> p)来改变模式1,他们可以确保旧代码的工作原理因为(不同于独特指针)自动指针可以被静默地解引用到一个值上(指针对象本身在进程中被重置为空)。 显然,委员会比模式1更喜欢提倡模式3,他们选择积极打破现有的代码,而不是使用模式1,即使已经被弃用的用法。

何时比模式1更喜欢模式3

模式1在许多情况下是完全可用的,并且在假设所有权将采取将智能指针移动到局部变量的形式的情况下可能优于模式3,如在上面的reversed示例中那样。 不过,在更一般的情况下,我可以看到有两个理由更喜欢模式3:

  • 传递参考比创建临时参数稍微更有效率,并且nix旧指针(处理现金有点费力); 在某些情况下,指针可能会在实际被窃取之前多次不变地传递给另一个函数。 这种传递通常需要编写std::move (除非使用模式2),但请注意,这只是一个实际上没有做任何事情(特别是没有取消引用)的转换,因此它没有附加任何成本。

  • 应该可以想象,在函数调用的开始和它(或某个包含的调用)实际上将指向对象移动到另一个数据结构的位置之间引发异常(并且此异常尚未被捕获到函数本身内部),那么当使用模式1时,由智能指针引用的对象将在catch子句可以处理异常之前销毁(因为函数参数在堆栈展开期间被破坏),但在使用模式3时不会如此。后者给出调用者可以选择在这种情况下恢复对象的数据(通过捕获异常)。 请注意,这里的模式1 不会导致内存泄漏 ,但可能会导致程序无法恢复的数据丢失,这可能也是不受欢迎的。

返回一个智能指针:始终按照价值

总结一个关于返回一个智能指针的字,大概指向创建供调用者使用的对象。 这并不是一种与将指针传入函数相比的情况,但为了完整性,我想坚持在这种情况下总是按值返回 (并且不要return语句中使用 std::move )。 没有人想要得到一个可能刚刚被忽略的指针的引用


是的,如果你在构造函数中通过value来获取unique_ptr ,你必须unique_ptr 。 明确性是一件好事。 由于unique_ptr是不可复制的(private copy ctor),所以你写的应该会给你一个编译器错误。


以下是将独特指针作为参数的可能方式,以及它们相关的含义。

(A)按价值

Base(std::unique_ptr<Base> n)
  : next(std::move(n)) {}

为了让用户调用它,他们必须执行以下操作之一:

Base newBase(std::move(nextBase));
Base fromTemp(std::unique_ptr<Base>(new Base(...));

按值来取一个唯一的指针意味着你正在指针的所有权转移给有问题的函数/对象/等等。 newBase构建完成后, nextBase保证为 。 你不拥有这个对象,并且你甚至没有指向它的指针了。 它消失了。

这是确保,因为我们通过值参数。 std::move实际上并没有移动任何东西; 这只是一个幻想演员。 std::move(nextBase)返回一个Base&& ,它是对nextBase的r值引用。 就是这样。

因为Base::Base(std::unique_ptr<Base> n)的值是通过值而不是r值引用的,所以C ++会自动为我们构造一个临时的。 它从Base&&创建一个std::unique_ptr<Base> ,我们通过std::move(nextBase)给出函数。 这个临时的构造实际上 nextBase的值移动到函数参数n

(B)由非常量值引用

Base(std::unique_ptr<Base> &n)
  : next(std::move(n)) {}

这必须在实际的l值(一个命名变量)上调用。 它不能像这样临时调用:

Base newBase(std::unique_ptr<Base>(new Base)); //Illegal in this case.

它的含义与任何其他使用非const引用的含义相同:该函数可能会或可能不会声明指针的所有权。 鉴于此代码:

Base newBase(nextBase);

不保证nextBase是空的。 它可能是空的; 它可能不会。 它真的取决于Base::Base(std::unique_ptr<Base> &n)想要做什么。 正因为如此,从功能特征来看,这并不是很明显,会发生什么; 你必须阅读实现(或相关文档)。

因此,我不会建议这个界面。

(C)由常量l值引用

Base(std::unique_ptr<Base> const &n);

我不显示实现,因为你不能const&移动。 通过传递一个const& ,你就是说这个函数可以通过指针访问Base ,但是它不能存储在任何地方。 它不能要求它的所有权。

这可能很有用。 不一定适合你的具体情况,但能够向某人传递一个指针并知道他们不能 (不违反C ++的规则,比如没有抛出const )声明它的所有权总是很好的。 他们不能存储它。 他们可以将它传递给其他人,但其他人必须遵守相同的规则。

(D)由r值参考

Base(std::unique_ptr<Base> &&n)
  : next(std::move(n)) {}

这或多或少与“通过非常量值参考”情况相同。 差异是两件事。

  1. 可以通过一个临时的:

    Base newBase(std::unique_ptr<Base>(new Base)); //legal now..
    
  2. 传递非临时参数时必须使用std::move

后者确实是这个问题。 如果你看到这一行:

Base newBase(std::move(nextBase));

你有一个合理的期望,在这一行完成后, nextBase应该是空的。 它应该已经被移出。 毕竟,你有那个std::move坐在那里,告诉你发生了移动。

问题是它没有。 不保证已被移出。 它可能已经被移除,但只有通过查看源代码才能知道。 你不能从函数签名中知道。

建议

  • (A)按值:如果你的意思是声明一个unique_ptr 所有权的函数,那就把它作为值。
  • (C)通过const l-value引用:如果你的意思是函数在该函数的执行期间只使用unique_ptr ,则可以通过const& 。 或者,将&const&传递给指向的实际类型,而不是使用unique_ptr
  • (D)通过r值引用:如果一个函数可能或不可以声明所有权(取决于内部代码路径),然后用&&&&它。 但我强烈建议不要在可能的情况下这样做。

如何操作unique_ptr

你不能复制一个unique_ptr 。 你只能移动它。 执行此操作的正确方法是使用std::move标准库函数。

如果您按值创建unique_ptr ,则可以自由移动。 但是,由于std::move移动并不实际发生。 请采取以下声明:

std::unique_ptr<Base> newPtr(std::move(oldPtr));

这实际上是两个陈述:

std::unique_ptr<Base> &&temporary = std::move(oldPtr);
std::unique_ptr<Base> newPtr(temporary);

(注意:上面的代码在技术上并没有编译,因为非临时的r值参考实际上不是r值,仅供参考)。

temporary只是对oldPtr的r值引用。 它在运动发生的newPtr构造函数中。 unique_ptr的移动构造函数(一个构造函数需要一个&&自己)是实际的运动。

如果你有一个unique_ptr值,并且你想把它存储在某个地方,你必须使用std::move来执行存储。


首先,你必须学会​​像一个语言律师一样思考。

C ++规范没有引用任何特定的编译器,操作系统或CPU。 它引用了一个抽象机器 ,它是实际系统的泛化。 在“语言律师”的世界里,程序员的工作就是为抽象机器编写代码; 编译器的工作是在具体的机器上实现该代码。 通过严格地对规范进行编码,无论今天还是现在50年,您都可以确信,您的代码将在任何使用兼容的C ++编译器的系统上进行编译和运行,而无需进行修改。

C ++ 98 / C ++ 03规范中的抽​​象机器基本上是单线程的。 因此,不可能编写关于规范的“完全便携”的多线程C ++代码。 这个规范甚至没有提到内存加载和存储的原子性或者加载和存储的顺序 ,更不用说像互斥体这样的东西。

当然,您可以在实践中为特定的具体系统编写多线程代码 - 例如pthread或Windows。 但是没有为C ++ 98 / C ++ 03编写多线程代码的标准方法。

C ++ 11中的抽象机器是多线程设计的。 它也有一个明确的记忆模型 ; 也就是说,它说明编译器在访问内存时可能做什么和不可以做什么。

考虑下面的例子,其中一对全局变量由两个线程同时访问:

           Global
           int x, y;

Thread 1            Thread 2
x = 17;             cout << y << " ";
y = 37;             cout << x << endl;

线程2输出什么?

在C ++ 98 / C ++ 03下,这甚至不是未定义的行为; 这个问题本身是没有意义的,因为标准没有考虑任何被称为“线索”的东西。

在C ++ 11下,结果是未定义行为,因为加载和存储通常不需要是原子的。 这看起来似乎没有太大的改进......而且本身并不是。

但是用C ++ 11,你可以这样写:

           Global
           atomic<int> x, y;

Thread 1                 Thread 2
x.store(17);             cout << y.load() << " ";
y.store(37);             cout << x.load() << endl;

现在事情变得更有趣了。 首先,这里的行为是定义的 。 线程2现在可以打印0 0 (如果它在线程1之前运行), 37 17 (如果它在线程1之后运行)或0 17 (如果它在线程1分配给x之后但在分配给y之前运行)。

它无法打印的是37 0 ,因为C ++ 11中原子加载/存储的默认模式是强制执行顺序一致性 。 这只是意味着所有加载和存储都必须按照您在每个线程中编写的顺序“发生”,而线程间的操作可以交错,但系统喜欢。 所以原子的默认行为既提供原子性 ,也提供加载和存储的顺序

现在,在现代CPU上,确保顺序一致性可能很昂贵。 特别是,编译器可能在这里的每个访问之间发出全面的内存障碍。 但是,如果你的算法可以容忍乱序加载和存储; 即,如果它需要原子性而不是排序; 即,如果它可以容忍37 0作为该程序的输出,那么你可以这样写:

           Global
           atomic<int> x, y;

Thread 1                            Thread 2
x.store(17,memory_order_relaxed);   cout << y.load(memory_order_relaxed) << " ";
y.store(37,memory_order_relaxed);   cout << x.load(memory_order_relaxed) << endl;

CPU越现代化,这种情况越有可能比前面的例子更快。

最后,如果你只需要保持特定的装载和存储顺序,你可以写:

           Global
           atomic<int> x, y;

Thread 1                            Thread 2
x.store(17,memory_order_release);   cout << y.load(memory_order_acquire) << " ";
y.store(37,memory_order_release);   cout << x.load(memory_order_acquire) << endl;

这将我们带回到有序的加载和存储 - 所以37 0不再是可能的输出 - 但它只需很少的开销即可完成。 (在这个微不足道的例子中,结果与完整的顺序一致性相同;在一个更大的程序中,它不会)。

当然,如果你想看到的唯一输出是0 037 17 ,你可以在原始代码周围包装一个互斥体。 但是,如果你已经读了这么多,我敢打赌你已经知道这是如何工作的,而且这个答案已经比我想要的更长了:-)。

所以,底线。 互斥体非常好,C ++ 11将它们标准化。 但有时出于性能方面的原因,您需要较低级别的基元(例如,经典的双重检查锁定模式 )。 新标准提供了像互斥锁和条件变量这样的高级小工具,并且还提供了低级小工具,如原子类型和各种风格的内存屏障。 因此,现在您可以完全按照标准指定的语言编写复杂的高性能并发例程,并且您可以确定您的代码将在今天的系统和未来的系统上进行编译和运行。

虽然坦率地说,除非你是专家,并且正在研究一些严重的低级代码,你应该坚持互斥和条件变量。 这就是我想要做的。

有关这些内容的更多信息,请参阅此博客文章





c++ arguments c++11 unique-ptr