12

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

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

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.

亚历克斯。

4

3 回答 3

4

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

我的结论从这里开始:

应用程序与框架/库开发

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

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

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

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

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

抽象到实现

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

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

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

抽象的实现

这是我的默认选择。已经在很多方面说过了。“框架应该被提取”、“提取直到你放弃”,甚至“约定优于配置”在概念上都有一些相似之处。

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

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

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

总结一下

正如我一直提醒自己的那样,您可以拥有的最佳最佳实践是驯服已知的最佳实践以适应您的问题,而不是相反。如果你不能做到这一点,那么,也许这个问题足够有趣,需要进一步关注,以及一些原创的想法。

于 2013-09-03T15:06:21.903 回答
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
    }
};

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

于 2013-09-08T20:25:30.357 回答
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

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

于 2013-09-08T19:59:39.037 回答