【问题标题】:Encapsulating a large number of parameters in C++14在 C++14 中封装大量参数
【发布时间】:2018-08-06 13:56:51
【问题描述】:

我想编写一个使用许多参数的函数,我将其称为abc。我有四种选择在 C++14 中实现它。

对于 2018 年的一个新的现代 C++ 项目,其中哪一种风格最符合 ISO C++ 的理念?其他风格指南推荐哪些风格?

面向对象的风格

class Computer {
    int a, b, c;
public:
    Computer(int a, int b, int c) : a(a), b(b), c(c) {}
    int compute(int) const {
        // do something with a, b, c
    }
};
...
const Computer computer(a, b, c);
int result = computer.compute(123);

优点:

  • C++ 程序员易于掌握

缺点:

  • 要在 map 或 fold 操作中计算事物,我们必须使用笨重的 [computer](int input){ return computer.compute(input); }

C 风格

struct ComputeParams {
    int a, b, c;
};

int compute(const ComputeParams &params, int input) {
    // do something with params.a, params.b, params.c
}
...
const ComputeParams params{a, b, c};
int result = compute(params, 123);

优点:

  • C 程序员易于掌握

缺点:

  • compute 的详细实现涉及调用 params.a 而不是 a
  • 冗长调用,每次都必须传入一个结构体。

函子风格

struct Computor {
    int a, b, c;
    int operator()(int input) const {
        // do something with a, b, c
    }
};
...
const Computor compute{a, b, c};
int result = compute(123);

优点:

  • 面向对象风格的所有优点,加上它看起来像一个函数
  • 可用于 map、fold 和 for_each 等函数式操作

缺点:

  • “函子”这个词看起来很时髦。

功能风格

auto genCompute(int a, int b, int c) {
    return [a, b, c](int input) -> int {
        // do something with a, b, c
    }
}
...
auto compute = genCompute(a, b, c);
int result = compute(123);

优点:

  • OCaml 程序员易于掌握
  • 可用于 map、fold 和 for_each 等函数式操作
  • 技术上与仿函数相同

缺点:

  • C++ 和 C 程序员很难掌握
  • 由于 lambda 函数是由编译器生成的唯一类型,因此可能需要使用 auto 或模板魔术来内联 lambda 函数,或使用 std::function 来增加性能开销
  • 不能接受 vtable 的强大功能和多态性继承

【问题讨论】:

  • 注意with uniform function call (pdf),上面的C风格和面向对象的风格变成了同一个东西。
  • “面向对象”和“函子”是一样的。当需要动态多态可调用对象时,只需添加 operator() 并将“doIt”或“compute”方法设置为从基类的(非虚拟)operator() 调用的“私有虚拟”。 OTOH 为什么函数必须在全局命名空间中?最好不要将 any 名称放入全局命名空间中。
  • 关于命名空间你是对的;一切都应该在他们自己的命名空间中。我的观点是,班级成员可以免费获得某种“命名空间”。

标签: c++ oop functional-programming c++14 encapsulation


【解决方案1】:

其中很多都是基于意见的,但我会把帽子扔在戒指上。

面向对象的风格

不是供您使用的风扇。由于您支持的唯一操作是compute,因此它实际上是operator (),名称不同。 operator () 意味着您可以很好地使用 algorithm 标头,因此这是 Functor 和 Functional 样式的劣质解决方案。

此外,您的代码可能在此解决方案中表现更差。如果您好奇的话,整个演讲都值得一看,但 Chandler Carruth(LLVM/Clang 开发人员)explains how the compiler sees your code(跳到大约 1:32:37,但整个演讲很棒)。它的要点是你在这个实现中有一个隐式指针,而指针/引用对于编译器来说更难优化。

C 风格

只是为了 API 的缘故,我不喜欢这个。您在缺点中提到调用需要传递struct,这在使用需要操作单个事物的库时会出现问题(例如,algorithm 中的所有内容)。您可以使用捕获您的struct 的 lambda 解决此问题,但那时我不知道您获得了什么。

功能风格

这就是我要走的路,也是我在工作中要推动的。我见过的例子表明,调用 lambda 函数并不比直接调用函数慢,因为编译器可以积极内联(它们知道确切的类型)。如果 C++ 程序员因为这种风格不同/新而苦苦挣扎,请告诉他们加快速度,因为它们落后了几个标准:)。

就最佳实践和社区使用的内容而言,来自 Cppcon 的示例似乎更倾向于函子/函数式风格。 C++ 作为一门语言,看起来它确实包含了一般的函数式设计。

【讨论】:

  • “面向对象风格”和“功能风格”在性能上有什么区别?难道 lambdas 不只是被转换为与 Functor/Object Oriented 样式中使用的结构/类等效的结构/类(在编译器内部)吗?
  • 我使用Compiler Explorer 快速浏览了一下,computer.compute(123); 的代码与compute(123); 相同;相关的计算/ lambda函数也是如此。唯一的区别是生成的代码在调用构造函数和调用创建 lambda 或 default-ctor 结构的函数之间的调用约定略有不同。
【解决方案2】:

对于函数式风格还有很多话要说:您可以轻松地为某些参数值提供特殊/优化的版本

std::function<int(int)> getCompute(int a, int b, int c)
{
    if(a==0)
        return [b,c](int input) { /* version for a=0 */ };
    if(b==0)
        return [a,c](int input) { /* version for b=0 */ };
    if(c==0)
        return [a,b](int input) { /* version for c=0 */ };
    /* more optimized versions */
    return [a,b,c](int input) { /* general version */ };
}

与其他选项等效的东西并不简单。不幸的是,这需要使用 std::function 来包装不同的 lambda。

【讨论】:

  • 不确定你是否仍然可以在你的示例中返回 auto,因为每个 lambda 都是不同的类型(我已经被这个咬住了),但你可以返回一个 std::function。不错的观察结果。
  • @StephenNewell 我自己也想知道,但你是对的。
【解决方案3】:

tl;dr:使用下面的代码 sn-p。

所谓的面向对象的选项

它不是面向对象的,它只是将东西粘在一个对象中。 “用 b 和 c 做某事”的函数不应该是包含它们的对象的成员;该功能不是 a、a b 和 a c 组合所固有的。

Functor 选项

对于所谓的面向对象选项的类似批评。只是在没有充分理由的情况下将仿函数插入其中。

功能选项

您实际上只是在使用 lambda 而不是构造结构...并不可怕,但请参阅下一个选项:

所谓的 C 风格选项 - 胜利

这在 C++ 中非常流行。结构是一个非常好的对象 - 具有公共数据成员和默认构造函数和析构函数。

此外,您的两个缺点都可以轻松解决,并且代码更短更简单:

struct ComputeParams {
    int a, b, c;
};

auto compute(ComputeParams params, int input) {
    auto [a, b, c] = params;
    // do something with a, b and c
}
auto result = compute(ComputeParams{a, b, c}, 123);

这是有效的 C++17(使用结构化绑定);在 C++14 中,您需要 std::tie 将参数绑定到本地名称。另外,请注意,我在引用语义上使用了更多的值语义,让编译器发挥它的魔力(它可能会这样做;即使对于一些ints 来说并不重要)。

这是我推荐的。

【讨论】:

  • 结构化绑定确实很棒!最后,我们不必像在 C++14 中那样做std::tie(...)。至于函子和函数,一个优点是能够将它们放在 std::function 中,并在各种 STL 函数中使用它们,例如在 std::sort 中将它们用作比较器。
  • @Daniel:关于将东西放入 std::function - 只需在需要时使用 lambda(例如,在某处传递 std::function);一般不需要getCompute()。无需先验地使您的生活复杂化。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-10-21
  • 2020-05-29
相关资源
最近更新 更多