移动语义和虚拟方法

时间:2013-09-03 14:40:12

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

在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.

亚历。

3 个答案:

答案 0 :(得分:4)

这里没有“一切理论”。你做对了,有问题。 我记得有一段时间我自己面对它。

我的结论从这里开始:

应用程序与框架/库开发

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

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

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

这产生了巨大的差异。因为我们应该更好地了解系统的运行方式,以及哪种代码可以变得有用。我们不是先知,但与此同时,这种假设使我们能够更加信任我们的直觉,这是基于我们对需求的了解和客户的需求(至少我们能够理解的那样) )。

此时,我们仍然可以将其分为两种情况:

抽象实施

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

一些极端的agiler会让你相信你可以写一些代码并在以后修复它。然而,一旦现实触及,这种说法很容易被伪造。如果您对它有希望,我建议您自己测试并报告您是否做了重大发现。我个人的经历,并且认为我已经尝试过这个问题,这表明在大项目中这种方法很成问题。

在这种情况下的结论是,如果您确实需要来提前定义抽象,那么您应该已经非常了解实现。你对它有了更好的想法,实际成为一个合适的抽象成功的可能性就越大。

实施抽象

这是我的默认选择。在很多方面都有人说过。 “框架应该被提取”,“提取”直到你丢弃“,甚至”Convention over Configuration“在概念上也有一些相似之处。

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

这通常会成为一个能够满足您需求的课程,但更多。在这种情况下,您将交叉点抽象为更一般的情况。在整个开发过程中,您需要重复此过程。

重要的是不要陷入困境,仍然保持脚踏实地。我已经看到许多抽象尝试出错了,除非要阅读使用它的数千行代码,否则无法推断其名称并推断其意图。例如,在我正在处理的当前代码库中,应该被称为Image的类型称为BinaryData。代码中的所有代码都试图将其视为具体(图像),同时作为抽象概念。

总结

我总是提醒自己,你可以拥有的最好的最佳实践就是驯服已知的最佳实践来适应你的问题,而不是相反。如果你不能这样做,那么,问题可能是有趣的,需要进一步关注,并有点原创思想。

答案 1 :(得分:2)

您还可以为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
    }
};

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

答案 2 :(得分:0)

我认为肯定依赖于实施。正如您的问题所暗示的那样,除了完全“总是更好”的签名之外,唯一明智的做法是以优化当前实现的方式选择签名。如果你在代码之前编写接口 - 采取有根据的猜测,并尝试以这样的方式操纵自己,以便在提交签名之前等待第一次实现。

这里的操作词是“第一”和“当前”。如果你弄错了怎么办?如果在稍后阶段签名会阻止您的代码达到最佳状态,会发生什么?这是你可以做的:

没有承诺

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

致力于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兼容性。)