【问题标题】:Move semantics and virtual methods移动语义和虚方法
【发布时间】:2013-09-06 20:30:38
【问题描述】:

在 C++11 中,我们被引导在某些情况下通过值传递对象,而在其他情况下通过 const-reference 传递对象。但是,此指南取决于方法的实现,而不仅仅是其接口和客户的预期用途。

当我写一个接口时,我不知道它会如何实现。编写方法签名是否有一个好的经验法则?例如——在下面的代码片段中,我应该使用Bar1还是Bar2

class IFoo
{
public:
    virtual void Bar1(std::string s) = 0;
    virtual void Bar2(const std::string& s) = 0;
};

如果您同意正确的签名取决于实现,则可以停止阅读此处。这是一个例子,说明了我为什么这么认为。

在下面的例子中,我们应该按值传递字符串:

class Foo
{
    std::string bar;

    Foo(std::string byValue)
        : bar(std::move(byValue))
    {
    }
};

现在我们可以在所有情况下以有效的方式实例化 Foo:

Foo foo1("Hello world"); // create once, move once
Foo foo2(s); // the programmer wants to copy s. One copy and one move
Foo foo3(std::move(t)); // the programmer does not need t anymore. No copy at all

在其他情况下,我们更喜欢通过 const 引用传递对象。例如,在以下情况下,我们不想复制/存储参数,只需使用它的方法:

void DoStuff(const std::string& byRef)
{
    std::cout << byRef.length() << std::endl;
}

上述方法的所有可能用法已经尽可能高效。

更新

我相信我忘了展示 const-reference 替代方案的问题。如果上面的类Foo是这样实现的:

class Foo
{
    std::string bar;

    Foo(const std::string& byRef)
        : bar(byRef)
    {
    }
};

那么我们会得到以下结果:

Foo foo1("Hello world"); // Here we would have one more copy of the string. It is less efficient.
Foo foo2(s);             // One copy, like before
Foo foo3(std::move(t));  // Irrelevant here.

亚历克斯。

【问题讨论】:

  • 如果要在启动方法后更改值(例如移动它),则应按值传递,否则应按引用传递。如果对象很小(小于指针大小),那么按值传递通常更有效。在任何一种情况下,使用您的 API 的人都会以相同的方式解释参数:该函数不会更改我传递给它的参数的值。
  • 很明显,你永远无法肯定知道它是如何实现的(除非你始终是执行实现的人),但通常函数的名称应该提供线索:如果该函数被称为 GetStringLength ,那么您可以确定它不会复制参数:P
  • @MadScienceDreams,你是对的。但是,我问的是函数希望从调用者的角度对其参数进行只读访问的情况(在内部它可能会移动一个按值对象,但调用者不知道这一点)。从效率的角度来看,实现这些功能的“正确”方式是不同的,具体取决于实现。我要问的是,是否有一些通用的方法可以编写既高效又不依赖于实现的纯虚函数。
  • @stijn 你可能是对的,但我没有什么技巧可以做的吗?有模板的东西?也许还有别的?
  • 假设我不想修改传递的变量并且它不是基本数据类型,我总是通过 const 引用传递。如果我需要在函数中修改它,只需在本地复制引用即可。

标签: c++ c++11 move-semantics pure-virtual


【解决方案1】:

这里没有“万物理论”。你没看错,有问题。 我记得不久前我自己也遇到过。

我的结论从这里开始:

应用程序与框架/库开发

如果您的客户是开发人员,那么这项工作就更难了。它不仅更难,而且没有明确的指导方针。伟大的框架设计师之所以享有声望,是因为他们碰巧承担了获得回报的风险。同时,在另一个宇宙中,他们的风险可能没有得到回报。那是因为欣赏一个框架取决于其不断增长的使用方向,以及比在应用程序领域更难推理的主观意见。

所以在这种情况下没有明确的答案。幸运的是,我认为您主要对这里的应用程序开发感兴趣。所以让我们开始吧。

起点:我们正在开发应用程序

这有很大的不同。因为我们应该对系统的走向有一个更好的了解,以及什么样的代码会变得有用。我们不是先知,但与此同时,这种假设使我们能够更多地相信我们的直觉,这是基于我们对需求和客户需求的了解(至少在我们能够理解的范围内) )。

此时,我们还是可以分为两种情况:

从抽象到实现

在某些情况下,在实现之前定义抽象是有益的,甚至是必要的。在这种情况下,必须意识到在正确定义抽象之前需要对问题进行更多研究。例如,域是同步的还是异步的?串行还是并行?高水平还是低水平?以及其他更具体的问题。

一些极端敏捷者会让您相信您可以编写一些代码并在以后修复它。然而,一旦现实发生,这种说法很容易被证伪。如果您从中找到希望,我鼓励您自己进行测试并报告您是否有任何重大发现。我个人的经验,以及我曾尝试过解决这个问题的想法表明,在大型项目中,这种方法非常有问题。

在这种情况下的结论是,如果您确实需要提前定义抽象,那么您应该已经对实现有了很好的了解。你对它有更好的想法,它实际上成为一个适当的抽象的机会就越大。

抽象的实现

这是我的默认选择。已经在很多方面说过了。 “Frameworks should be extract”、“Extract 'til you drop”,甚至“Convention over Configuration”在概念上都有一些相似之处。

基本上,这意味着您可以根据需要实施所需的组件,但要密切注意正在发生的事情。这里的诀窍是寻找机会以在开发和维护方面实际上对您有益的方式进行抽象。

这通常是作为一个可以做你想做的事的类,但更多。在这种情况下,您将交集抽象为更一般的情况。您可以在整个开发过程中根据需要重复此过程。

重要的是不要被赶上并仍然脚踏实地。我已经看到许多抽象尝试都出错了,以至于除了阅读使用它的数千行代码外,无法推断其名称并推断其意图。例如,在我正在处理的当前代码库中,应该称为Image 的类型称为BinaryData。整个代码都试图将其视为一个具体的(图像),同时又是一个抽象的概念。

总结一下

我总是提醒自己,最好的最佳实践是驯服已知的最佳实践以适应您的问题,而不是相反。如果你做不到,那么,也许这个问题很有趣,需要进一步关注,需要一些原创性的想法。

【讨论】:

  • 对于同一抽象的两种不同实现,但在效率方面哪个接口更好?,您有什么建议?
  • @Alex 抽象在现实生活中的概念可能相同,但这里的规则不同(你知道旧的矩形/正方形示例)。您的问题实际上太笼统,无法简单回答。在我看来,这是一个上下文和权衡的问题。尝试一个想法 - 不好? - 尝试另一个。重复直到找到听起来最好(或最不坏)的东西。如果您对所有这些都不满意,请尝试对问题进行不同的概念化。有没有办法实现清洁的解决方案?大概;但没有人说这些说明是写在书上的。
  • 如果你还想要一个更具体的答案,请提供一个更具体的例子,包括要求等。就像我说的,在这里设计一个简单的通用解决方案几乎是不可能的。
【解决方案2】:

我相信它应该肯定取决于实施。正如您的问题所暗示的那样,除非完全“总是更好”的签名,否则唯一明智的做法是以优化当前实现的方式选择签名。如果您在代码之前编写接口 - 进行有根据的猜测,并尝试以这样一种方式操纵自己,以便您可以在提交签名之前等待第一个实现。

这里的关键词是“第一个”和“当前”。如果你弄错了会发生什么?如果在稍后阶段签名阻止您的代码处于最佳状态,会发生什么?您可以执行以下操作:

没有承诺

如果它足够快 - 只需改变它。它遵循“不承诺”的定义,对吧?

致力于 API

举个具体的例子,假设你选错了,然后继续:

virtual void DoStuff(std::string s) = 0;

但是,事实证明,不需要执行复制(与您最初的 DoStuff 实现相同)。您可以执行以下操作:

// stuff.h
virtual void DoStuff_Optimized(const std::string & s);
virtual void DoStuff(std::string s);

// stuff.cc
virtual void DoStuff_Optimized(const std::string & s);
{
    // Fast implementation of DoStuff, no copying necessary
    std::cout << s.length() << std::endl;
}

virtual void DoStuff(std::string s)
{
    DoStuff_Optimized(s);
}

现有客户将获得较差的性能。新客户可以使用Optimized 版本。

致力于 ABI

不幸的是,此时您可能无能为力。但是,如果您是 careful,您也许可以遵循“致力于 API”操作。 (特别是,我的示例不会保留 ABI 兼容性)。

【讨论】:

  • 当您有两个具有不同“最佳”签名的相同接口实现并且都在代码中广泛使用时,就会出现问题。例如,在指向基数的指针向量中,大约一半元素希望通过值获取参数,另一半通过 const 引用获取参数以获得最佳性能。
  • 所以? API 文档(.h 文件)应该明确哪个是最佳的。作为 API 设计者,您需要做出决定。最佳版本应该没有歧义。
  • 我将再次尝试解释 - 一个接口 A。两个实现 - B 和 C。接口 A 的方法签名对 B 有利,对 C 不利。所以在 B 或 C 的头文件中记录一些内容没有帮助。我仍然只有 一个 接口,并且在效率方面它不能很好地服务于这两种实现。如果我有一个 A* 向量,其中一半是 B 类型的元素,其中一半是 C 类型的元素 - 一半向量的性能可能很差。问题 - 有没有办法只拥有一个界面并且仍然具有良好的性能?
【解决方案3】:

您还可以为Bar2 提供一个采用右值引用的重载:

class IFoo
{
public:
    virtual void Bar2(const std::string& s) = 0;

    virtual void Bar2(std::string&& s)
    {
        Bar2(s);   // calls the const& overload because s is an lvalue
    }
};

默认情况下,右值引用重载只是调用 const 左值引用重载。但是如果特定的子类可以利用右值引用,则可以覆盖右值引用重载。

【讨论】:

  • 我想说提供 rvalue-ref 版本是有意义的只有如果您将值“沉入”到拥有的存储中(在“Sean Parent”中)。即便如此,在存在许多此类参数的情况下,按值取值会更有效(因为右值组合的爆炸式增长)
猜你喜欢
  • 2016-02-04
  • 2020-08-20
  • 2012-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-11-11
相关资源
最近更新 更多