在构造函数中传递指针/引用到现有对象的首选方式是什么?

16

我将从一个例子开始。在boost库中有一个很好的"tokenizer"类。它接受一个要被分割成标记的字符串作为构造函数的参数:

std::string string_to_tokenize("a bb ccc ddd 0");
boost::tokenizer<boost::char_separator<char> > my_tok(string_to_tokenize);
/* do something with my_tok */
字符串在令牌化过程中不会被修改,因此它通过const对象引用传递。因此我可以在那里传递一个临时对象:

字符串在令牌化过程中不会被修改,因此通过const对象引用传递。因此,我可以在那里传递一个临时对象:

boost::tokenizer<boost::char_separator<char> > my_tok(std::string("a bb ccc ddd 0"));
/* do something with my_tok */

一切看起来都很好,但是如果我尝试使用分词器(tokenizer),就会出现问题。经过简短的调查,我意识到,分词器类存储了我给它的引用,并在以后使用。当然,对于临时对象的引用,它无法正常工作。

文档没有明确说明在构造函数中传递的对象将在以后使用,但是好吧,也没有说明不会:)所以我不能假定这一点,这是我的错误。

然而,这有些令人困惑。在一般情况下,当一个对象通过const引用接收另一个对象时,可以传递临时对象。你认为这是不好的惯例吗?在这种情况下,也许应该使用指向对象的指针(而不是引用)?甚至更进一步——是否有用的特殊关键字来允许/禁止将临时对象作为参数传递呢?

编辑:文档(版本1.49)非常简洁,唯一可能暗示此问题的部分是:

注意:实际上,在构造期间并不执行任何解析。解析是在通过由begin提供的迭代器访问标记时按需执行的。

但它没有明确说明将使用给定的同一对象。

然而,这个问题的重点在于讨论这种情况下的编码风格,这只是激发我的一个例子。


被同样的事情咬了一口。现在我能做到的是,使用boost::ref作为构造函数参数,以至少提示将存储引用。 - Anycorn
如果boost::tokenizer真的存在这样的bug,我会感到惊讶。 - CashCow
1
@CashCow:这更像是文档中的一个错误,因为“tokenizer”在其生命周期内保留对其构造函数参数的引用,这在使用临时变量时非常麻烦... - Matthieu M.
如果数据量很大,这样做可能会有优势,但总体而言对用户来说会很困惑。如果我这样做,即使我不打算更改数据,我也会故意将非const引用作为参数,只是为了捕捉到这个特定的用户错误。 - CashCow
4个回答

11
如果某个函数(例如构造函数)将参数作为对const的引用,则应该清楚地说明所引用的对象的生命周期必须满足某些要求(比如“在这个时刻之前不能被销毁”等)或者在需要在稍后使用给定对象时在内部创建副本。在这种特殊情况下(boost::tokenizer 类),我认为出于性能原因和使类可用于一些本身不可复制的容器类型,可能没有执行后者。因此,我认为这是一个文档错误。

8

我个人认为这是一个不好的想法,最好的方法是写构造函数以复制字符串或者使用const std::string*。对于调用者来说,只需要多输入一个字符,但这个字符可以防止他们意外地使用临时对象。

总的来说:不要让人们承担维护对象的责任,除非非常明显地告诉他们他们有这个责任。

我认为一个特殊的关键字并不能完全解决问题,从语言层面上进行修改并不值得。实际上,问题不在于临时对象,而是任何生存时间比正在被构造的对象短的对象。在某些情况下,临时对象是可以接受的(例如如果tokenizer对象本身也是同一表达式中的临时对象)。我不想仅仅为了半个修复而对语言进行修改,因为还有更全面的解决方案可供选择(例如使用shared_ptr,但它也有自己的问题)。

"所以我不能假设这个,我的错误"

我认为这并不是你的错,我同意Frerich的观点,如果你这样做并且没有记录,则在任何合理的样式指南中都是文档错误。

如果函数参数的引用生命周期不是"至少与函数调用一样长",那么必须记录下来,这是绝对必要的。文档通常会忽视这个问题,需要正确地处理才能避免错误。

即使在垃圾回收语言中,生命周期本身也是自动处理的,因此往往会被忽略,但是是否可以更改或重用对象而不更改其它在过去的某个时间将其传递给方法的对象的行为也很重要。因此,在任何缺乏引用透明度的语言中,函数都应该记录它们是否保留了其参数的别名,特别是在C++中,对象生命周期是调用者的问题。

不幸的是,确保函数不能保留引用的唯一机制是按值传递,这会带来性能成本。如果你可以发明一种语言,允许正常使用别名,但也有一个类似于C风格的restrict属性,在编译时强制执行,类似于const,以防止函数存储其参数的引用,那么祝你好运并加入我吧。


我同意你的观点,并感到惊讶它被加入了boost库。我会通过非const引用来获取参数,以确保用户不会传递临时变量。我也可以将其存储为std::string成员,并与传递进来的参数进行交换,如果用户希望保留原始字符串,则让他们自己创建副本。 - CashCow
元评论:为什么这是一个社区维基回答?无论如何,我会给它加上+1的赞。 - Francesco
@Francesco:我已经放弃尝试弄清楚SO上的话题是什么了,这是其他人要决定的。但通常我不会因为意见问题而获得声望。 - Steve Jessop

3
正如其他人所说,boost::tokenizer示例可能是tokenizer中的一个错误或文档缺少警告的结果。
总体而言,我发现以下优先级列表很有用。如果由于某些原因无法选择选项,则转到下一项。
  1. 按值传递(可以复制并且不需要更改原始对象)
  2. 按const引用传递(不需要更改原始对象)
  3. 按引用传递(需要更改原始对象)
  4. 按shared_ptr传递(对象的生命周期由其他东西管理,这也清楚地显示了保留引用的意图)
  5. 按原始指针传递(您获得要转换的地址,或者由于某种原因无法使用智能指针)
此外,如果您选择列表中的下一个项目的理由是“性能”,则请坐下来测量差异。根据我的经验,大多数人(特别是具有Java或C#背景的人)倾向于高估通过值传递对象的成本(并低估解除引用的成本)。按值传递是最安全的选项(它不会在对象或函数之外甚至在另一个线程中引起任何意外),不要轻易放弃这个巨大的优势。

1
很多时候它取决于上下文,例如如果它是一个函数对象,在for_each或类似情况下被调用,那么你通常会在函数对象中存储对一个对象的引用或指针,你预期这个对象的生命周期将超出你的函数对象。
如果它是一个通用类,则必须考虑人们如何使用它。
如果您正在编写一个词法分析器,您需要考虑复制正在标记化的内容可能是昂贵的,但您还需要考虑到如果您正在编写一个boost库,您正在为将以多种方式使用它的公众编写该库。
在这里存储const char *std::string const&好。如果用户有一个std::string,那么const char *只要他们不修改字符串,就会保持有效,而他们可能不会。如果他们有一个const char *或持有字符数组并将其传递进来的其他东西,它将无论如何复制以创建std::string const &,您非常危险,因为它不会在构造函数之后存在。

当然,使用const char *时,您无法在实现中使用所有可爱的std::basic_string函数。

有一个选项可以作为参数采用std::string&(非const引用),这应该可以保证(使用符合规范的编译器)没有人会传递临时对象,但您将能够记录您实际上并未更改它的事实,并解释您看似不正确的const代码背后的原理。请注意,我也曾在我的代码中使用过这个技巧。您可以愉快地使用字符串的查找函数。(如果您希望,还可以采用basic_string而不是string,以便您也可以对宽字符字符串进行标记化处理)。


我同意非const引用可以帮助避免这种错误,但这只是一种解决方法。这显然不是一个干净的解决方案,对吧? - peper0
在我看来,“const char *”在大多数情况下并不能解决问题,因为我仍然可以传递指向临时对象的string(something).c_str()。 - peper0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接