【问题标题】:Why aren't named parameters used more often?为什么不经常使用命名参数?
【发布时间】:2012-04-19 17:26:07
【问题描述】:

我设计了一个参数类,它允许我编写如下代码:

//define parameter
typedef basic_config_param<std::string> name;

void test(config_param param) {

  if(param.has<name>()) { //by name
    cout << "Your name is: " << param.get<name>() << endl;
  }

  unsigned long & n = param<ref<unsigned long> >(); //by type
  if(param.get<value<bool> >(true)) { //return true if not found
    ++n;
  }
}


unsigned long num = 0;
test(( name("Special :-)"), ref<unsigned long>(num) )); //easy to add a number parameter
cout << "Number is: " << num; //prints 1

该类的性能非常快:所有内容都只是堆栈上的一个引用。为了保存所有信息,我在进行堆分配之前使用最多 5 个参数的内部缓冲区来减小每个对象的大小,但这很容易改变。

为什么不经常使用这种语法,重载operator,() 来实现命名参数?是因为潜在的性能损失吗?

另一种方法是使用命名习语:

object.name("my name").ref(num); //every object method returns a reference to itself, allow object chaining.

但是,对我来说,重载operator,() 看起来更“现代”C++,只要你不要忘记使用双括号。即使它比正常功能慢,性能也不会受到太大影响,因此在大多数情况下可以忽略不计。

我可能不是第一个提出这种解决方案的人,但为什么它不更常见?在我编写一个接受它的类之前,我从未见过像上面的语法(我的例子)这样的东西,但对我来说它看起来很完美。

【问题讨论】:

  • ...因为它增加了复杂性而且没那么有用?
  • 这实现了什么?
  • 另外,重载operator, 通常是个坏主意,因为它的行为不像普通的, 运算符(类似的逻辑解释了为什么不应该重载operator&amp;&amp;operator|| )。
  • 是的,我的意思是,我看到这里有很多复杂性,但几乎没有收获。记住;复杂性要花钱。人们需要时间来了解正在发生的事情($$$),并且需要时间来维护/修复与之相关的错误(再次,$$$)。老实说,如果我在我工作的代码中看到类似的东西,我将不得不与实现它的人聊天。这对我来说有点像没有经验的开发者会想出的东西。
  • 这个功能已经在 boost 中使用多年了:boost.org/doc/libs/1_49_0/libs/parameter/doc/html/index.html 不知道实际使用了多少

标签: c++ named-parameters


【解决方案1】:

我的问题是为什么不更多地使用这种语法,重载 operator,() 来实现命名参数。

因为它违反直觉、不可读,并且可以说是一种糟糕的编程习惯。除非您想破坏代码库,否则请避免这样做。

test(( name("Special :-)"), ref<unsigned long>(num) ));

假设我第一次看到这个代码片段。我的思考过程是这样的:

  1. 乍一看,它看起来像是"the most vexing parse" 的示例,因为您使用了双括号。所以我假设 test 是一个变量,并且想知道你是否忘记写变量的类型。然后我突然想到这个东西实际上可以编译。在那之后,我不得不怀疑这是否是一个立即销毁的类型测试类的实例,并且您对所有类类型都使用小写名称。
  2. 然后我发现它实际上是一个函数调用。太好了。
  3. 代码片段现在看起来像一个带有两个参数的函数调用。
  4. 现在对我来说很明显,这不可能是带有两个参数的函数调用,因为您使用了双括号。
  5. 所以,现在我必须弄清楚 () 内部到底发生了什么。
  6. 我记得有一个逗号运算符(在过去的 5 年中我从未在真正的 C++ 代码中看到过)它丢弃了前面的参数。所以现在我不得不想知道 name() 的有用副作用是什么,以及 name() 是什么 - 函数调用或类型(因为您不使用大写/小写字母来区分类/函数(即Test 是一个类,但 test 是一个函数),并且您没有 C 前缀)。
  7. 在源码中查找name,发现是class。并且它重载了, 运算符,因此它实际上不再丢弃第一个参数。

看看这里浪费了多少时间?坦率地说,写这样的东西会给你带来麻烦,因为你使用语言特性使你的代码看起来与你的代码实际做的不同(你用一个参数进行函数调用看起来有两个参数,或者它是一个可变参数函数)。这是一种糟糕的编程习惯,大致相当于重载 operator+ 以执行减法而不是加法。

现在,让我们考虑一个QString 示例。

 QString status = QString("Processing file %1 of %2: %3").arg(i).arg(total).arg(fileName);

假设我有生以来第一次看到它。这就是我的思考过程:

  1. 有一个 QString 类型的变量 status
  2. 它是从 QString() 类型的临时变量初始化的。
  3. ... 在调用 QString::arg 方法之后。 (我知道这是一种方法)。
  4. 我在文档中查找.arg 以查看它的作用,发现它替换了%1 样式的条目并返回QString&。所以.arg() 调用链立即有意义。请注意,可以对 QString::arg 之类的东西进行模板化,您可以为不同的参数类型调用它,而无需在 &lt;&gt; 中手动指定参数的类型。
  5. 该代码片段现在可以理解了,所以我转到另一个片段。

看起来更“现代”的 C++

“新的和闪亮的”有时意味着“有缺陷的和损坏的”(slackware linux 建立在一个有点类似的想法上)。如果您的代码看起来很现代,这无关紧要。它应该是人类可读的,它应该做它打算做的事情,并且你应该浪费尽可能少的时间来编写它。 IE。您应该(个人建议)旨在“以最少的成本(包括维护)在最少的时间内实现最多的功能”,但这样做可以获得最大的回报。遵循 KISS 原则也是有意义的。

您的“现代”语法不会降低开发成本,不会减少开发时间,并且会增加维护成本(违反直觉)。因此,应该避免这种语法。

【讨论】:

  • 这是一个非常好的答案,它清楚地展示了提问者的“技巧”将永远给每个不得不查看令人困惑以至于误导的东西的人带来严重的维护噩梦。干得好:+1。
  • 你的论点的很大一部分归结为鸡和蛋的问题——人们不使用它,因为它的含义不是很明显,但它的含义不是很明显,因为它没有被使用经常。我想有更好的理由避免它,而不是让人们在以前没有见过类似的东西时不立即理解它(如果这本身就是一个充分的理由,我想新的语言/语言结构将非常罕见)。跨度>
  • returns QString&amp; 你可以在 xvalues 上做到这一点??
【解决方案2】:

没有必要。您的动态调度(行为不同,取决于参数的 逻辑 类型)可以通过模板专业化实现 a) 更容易和 b) 更快。

如果您确实需要基于仅在运行时可用的信息进行区分,我会尝试将您的 test 函数移动为 param 类型的虚拟方法并简单地使用动态绑定(这就是它是为了,这就是你要重塑的东西)。

这种方法更有用的唯一情况可能是多调度场景,您希望减少代码并找到一些相似模式。

【讨论】:

  • 感谢您的回答,但定义“更容易”,对我来说它是不是很简单,几乎是一个简单的功能。速度也没有那么慢,因为所有内容都保存在连续的内存中,通常 100% 在堆栈上。
  • @Fredrik:您对序列运算符, 的使用很不寻常,而且一切都不是那么简单。运算符重载非常容易被误用,这是一种误用。在这方面,它与简单的使用相反。另外我想你的参数类型有一些魔力,这不是微不足道的,所以它实现并不简单。我的意思是较慢的是;您拥有处理器非常讨厌的所有这些分支指令,这很糟糕。使用模板时你没有,相反,如果我做对了,你有很多这种方法缺乏的优化机会。
  • 另外,请注意“慢”是一个相对术语。如果它有助于您的可读性/可维护性,请不要担心效率(好吧,也许是一些小驴子,也许是老鼠,这取决于您的项目)。话说回来;如果一个操作有点慢,可能加起来令人难以置信,如果该操作非常频繁地完成(例如:如果你正在实现DES,你甚至不会注意到缓慢的 CLI-args解析,但你的调度程序应该很快)。
猜你喜欢
  • 2016-05-16
  • 1970-01-01
  • 2021-11-06
  • 2020-08-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多