【问题标题】:How string accepting interface should look like?字符串接受界面应该是什么样子?
【发布时间】:2011-01-09 17:43:22
【问题描述】:

这是this question 的后续行动。假设我编写了一个接受或返回 const 字符串的 C++ 接口。我可以使用 const char* 以零结尾的字符串:

void f(const char* str); // (1)

另一种方法是使用 std::string:

void f(const string& str); // (2)

也可以编写一个重载并接受两者:

void f(const char* str); // (3)
void f(const string& str);

甚至是与提升字符串算法结合使用的模板:

template<class Range> void f(const Range& str); // (4)

我的想法是:

  • (1) 不是 C++ 语言,并且在后续操作可能需要知道字符串长度时效率可能较低。
  • (2) 不好,因为现在f("long very long C string"); 调用了涉及堆分配的 std::string 构造。如果f 使用该字符串只是将其传递给一些需要 C 字符串的低级接口(如 fopen),那么这只是浪费资源。
  • (3) 导致代码重复。尽管一个f 可以调用另一个,这取决于什么是最有效的实现。但是,我们不能基于返回类型重载,例如 std::exception::what() 返回 const char*。
  • (4) 不适用于单独编译,可能会导致更大的代码膨胀。
  • 根据实现的需要在 (1) 和 (2) 之间进行选择,这会将实现细节泄露给接口。

问题是:首选的方式是什么?我可以遵循任何单一的指导方针吗?你有什么经验?

编辑:还有第五个选项:

void f(boost::iterator_range<const char*> str); // (5)

它具有(1)(不需要构造字符串对象)和(2)(字符串的大小显式传递给函数)的优点。

【问题讨论】:

  • 在情况 (2) 中将没有堆分配。字符串将在堆栈上构造
  • @nice:对,std::string 本身是在堆栈上分配的。但是如果你的字符串足够长或者你的实现没有使用短字符串优化,那么 std::string 将在堆上分配它的存储空间。
  • 我认为只有在调用 std::string 复制构造函数时才会发生堆分配
  • @nice:那你错了。欢迎您重载 new 并自己验证(不要忘记使用“looooong 字符串”)。
  • "accepts" 和 "returns" 可能应该单独考虑(单独的选项,我的意思是,不一定是单独的问题),因为如果您 returning 一个字符串,那么您有一个内存管理问题要处理。就我个人而言,我不在乎一个函数是否接受 const char* 并且我有一个 string 来传递它,因为它在调用代码中是一个微不足道的区别。另一种方式是琐碎的代码,即使不一定是琐碎的性能。但是,如果它返回一个const char*,那么我必须担心谁释放它,而如果它返回一个string(对象,而不是引用),我不会。

标签: c++


【解决方案1】:

如果您正在处理纯 C++ 代码库,那么我会选择 #2,而不用担心函数的调用者不会将它与 std::string 一起使用,直到出现问题。与往常一样,除非出现问题,否则不要太担心优化。让您的代码干净、易读、易扩展。

【讨论】:

  • 但是,为什么你更喜欢 2,例如1?它不会更简洁、更易于阅读或更易于扩展!
  • @ybungalobill:因为如果我正在编写 C++,我宁愿处理 C++ 结构,除非我遇到需要开始解决的性能问题。
  • @ybungalobill const char* str 根据定义是一个指向字符的指针。这是一个 string 只是按照惯例。这就是为什么在 C++ 中,2 更干净。
  • @Mark,C++ 的一大优点是您不必将自己限制在一种严格的方法上——使用const char* 并不意味着它是糟糕的 C++,它只是一种不同的风格,只要有意义就没有本质上的错误!
  • @Nim:这是一个意见问题,所以我们都会有不同的意见 :) 除非必须,否则我宁愿不处理 C++ 中的指针。
【解决方案2】:

您可以遵循一条准则:使用 (2),除非您有充分的理由不这样做。

const char* str 作为参数并没有明确说明,str 上允许执行哪些操作。在段错误之前可以增加多少次?它是指向charchars 数组还是 C 字符串(即以零结尾的 char 数组)的指针?

【讨论】:

  • 您也可以遵循指导原则:除非您有充分的理由不这样做,否则请使用 (1)。你能证明你的指导方针是否合理吗?
  • 所有有效的关注点;为什么不把它们放在你的答案中?
  • @Charles Bailey 我没有想到这一点。我现在将评论移到答案中。
【解决方案3】:

我真的没有一个硬性偏好。根据具体情况,我会在您的大多数示例之间交替使用。

我有时使用的另一个选项类似于您的 Range 示例,但使用普通的旧迭代器范围:

template <typename Iter>
void f(Iter first, Iter last);

它有一个很好的特性,它可以轻松处理 C 风格的字符串(并允许被调用者在恒定时间内确定字符串的长度)以及 std::string

如果模板有问题(可能是因为我不希望函数在标头中定义),我有时也会这样做,但使用 char* 作为迭代器:

void f(const char* first, const char* last);

同样,它可以与 C 字符串和 C++ std::string 一起使用(我记得,C++03 没有明确要求字符串是连续的,但是我知道的每个实现都使用连续分配的字符串,我相信 C++0x 会明确要求它)。

因此,除了支持两个主要字符串之外,这些版本都允许我传达比普通 C 样式 const char* 参数(丢失有关字符串长度的信息,并且不处理嵌入的空值)更多的信息以惯用的方式类型(可能还有你能想到的任何其他字符串类)。

当然,缺点是你最终会得到一个额外的参数。

不幸的是,字符串处理并不是 C++ 真正的强项,所以我认为没有单一的“最佳”方法。但迭代器对是我倾向于使用的几种方法之一。

【讨论】:

  • +1。单个参数范围的优点是它允许从 std::string 和 C 字符串自动转换,因此用户的代码仍然像以前一样简单:f("hello")。使用两个参数是不可能的。我想知道为什么 C++0x 标准对 fstream::open... 没有朝这个方向做任何事情...
  • 单参数范围的问题是,如果您希望它与 C 字符串 Just Work 一起使用,则会依赖 Boost.Range。但你是对的,这样的语法肯定更方便。
  • 不一定是升压范围。 Boost range 只是一个例子,它也不会提供从 std::string 的自动转换,所以无论如何都会定义一些派生类型。然而,仔细想想,这种方法还有另一个问题:与(1)不同,(5)没有零终止语义。这意味着当您将它与期望零终止字符串的低级函数一起使用时,它并不能完全解决问题。在这种情况下,您无论如何都需要创建一个零终止副本。有什么想法吗?
  • 是的,我不愿提及对 Boost 的依赖,因为它不一定依赖于 boost。但是您确实需要对 some 范围实现的依赖。如果您确实需要使用以 null 结尾的字符串,我更愿意在呼叫站点处理它。调用strlen 一次以查找空值,然后基于它生成一个范围(或迭代器对),我可以将其传递给函数。这样,合约就变得更清晰了,函数参数的含义也没有歧义。
【解决方案4】:

为了获取参数,我会选择最简单的方法,通常是const char*。这适用于零成本的字符串文字,并且从存储在 std:string 中的内容中检索 const char* 通常成本也非常低。

就个人而言,我不会为超载而烦恼。除了最简单的情况之外,在所有情况下,您都希望合并到两个代码路径,并在某个时候让一个调用另一个,或者两者都调用一个公共函数。可以说,过载会隐藏一个是否转换为另一个,以及哪条路径的成本更高。

只有当我真的想在函数内部使用std::string 接口的const 功能时,我才会在接口本身中使用const std::string&amp;,我不确定仅使用size() 就足够了理由。

在许多项目中,无论好坏,经常使用替代字符串类。其中许多,如std::string 提供对零终止const char* 的廉价访问;转换为 std::string 需要一个副本。在接口中要求const std::string&amp; 指示存储策略,即使函数的内部不需要指定它也是如此。我认为这是不可取的,就像采用 const shared_ptr&lt;X&gt;&amp; 指示存储策略一样,而采用 X&amp;,如果可能的话,允许调用者对传递的对象使用任何存储策略。

const char* 的缺点是,纯粹从接口的角度来看,它不强制非空(尽管在某些接口中偶尔会使用空参数和空字符串之间的区别 - 这可以'不能使用std::string),const char* 可能只是单个字符的地址。然而,在实践中,使用const char* 传递字符串是如此普遍,以至于我认为这是一个相当微不足道的问题。其他问题,例如接口文档中指定的字符编码(适用于std::stringconst char*)是否更为重要并且可能会导致更多工作。

【讨论】:

  • 我一直更喜欢普通的 const char*,原因和你一样。顺便说一句,还有第五个选项:void f(boost::iterator_range str),它不指定存储策略,但与 std::string 一样有效。我只是没有检查代码变得多么干净。
  • @ybungalobill:您添加了对 boost 的依赖,对许多人来说这是一个不小的问题。
  • 这只是一个想法。它可能是其他任何东西,例如 std::pair 或者可能实现您自己的轻量级包装器。
  • 我个人更喜欢const string&amp;,原因与您经常喜欢const char* 的原因相同——因为它最简单。在实践中最简单,因为您可以使用string 对象或文字字符串或const char* 调用该函数。它会在前两种情况下创建一个临时的,但我让分析器告诉我我需要担心什么,性能方面。我的情况通常不是这样。
  • 接受主要是因为您确定了 std::string 的问题“即使函数内部不需要指定存储策略......”。这让我开始认真考虑使用范围。
【解决方案5】:

答案应该很大程度上取决于您打算在f 中做什么。如果您需要对字符串进行一些复杂的处理,则方法 2 是有意义的,如果您只需要传递给其他一些函数,然后根据这些其他函数进行选择(假设您正在打开一个文件 - 什么会最有意义?;))

【讨论】:

  • 这泄露了一个实现细节。这正是我想要避免的。
  • ...真的吗?如何? const char* 上面的 const std::string&amp; 告诉你什么 - 或者另一个隐藏了什么?
  • @Nim,假设明天我改变我的实现,所以首选另一种方式。据你说,我需要改变我的界面。这泄露了一个实现细节:“这个函数在内部使用 XXX,所以每次它从 XXX 变为 YYY 时,接口都会相应地改变”。
  • @ybungalobill,呃,你为什么要这么做?没有提及后续更改 - 无论如何,将这样的更改传播到界面似乎毫无意义!我要指出的是,您应该根据您在f 中所做的事情做出决定,但是一旦您定义了一个接口 - 将其更改为这样的东西是没有意义的。注意:除此之外,在不破坏现有代码的情况下,可以从接受 const char* 的函数更改为 const std::string&amp;,但反之则不行......
  • @Nim:“不破坏现有代码” - 不完全是。如果我提供给函数的参数表达式是用户定义的到const char* 的转换,那么将函数更改为const std::string&amp; 会破坏我的代码,因为现在需要强制转换的用户定义的转换太多。不寻常的情况,但如果您要声明组件的接口兼容性,那么这很重要。
【解决方案6】:

也可以写一个 重载并接受两者:

void f(const string&amp; str) 已经接受两者,因为从 const char*std::string 的隐式转换。所以#3 与#2 相比几乎没有优势。

【讨论】:

  • 它避免了转换。它允许实现决定哪个版本更好。
【解决方案7】:

如果函数体不做char-analysis,我会选择void f(const string&amp; str);表示它不是指char*str

【讨论】:

  • char-anaysis 到底是什么意思? std::stringconst char* 更适合分析其组成部分 char 吗?
  • @Charles :如果函数体对字符串的字符进行操作(更像是解析),那么为什么要传递string 开头呢?
  • 我认为您的意思是在只读意义上进行操作,因为问题中的两种选择都是只读的;读取std::string 的字符与读取char 数组(通过const char* 传递)一样容易吗?确实可以使用相同的[] 语法?事实上,如果你不看它的内容,为什么还要传递参数呢?
  • @Charles : [] 在字符串的情况下需要函数调用。如果函数体的整个业务都在玩字符,我认为这会有点慢。
  • 你测试过函数调用的开销吗?我刚刚编译了一个函数,该函数从const 引用传入的std::string 返回任意字符,并将其编译为两个mov 和一个ret 指令,没有实际的call。此外,我虽然你支持在界面中使用std::string,现在你让我支持吗?
【解决方案8】:

使用 (2)。

第一个问题不是问题,因为无论如何都必须在某个时候创建​​字符串。

担心第二点是过早优化的味道。除非您有堆分配有问题的特定情况,例如使用字符串文字的重复调用,并且这些无法更改,否则最好有利于清晰而不是避免这个陷阱。只有这样,您才可以考虑选项 (3)。

(2) 清楚地传达函数接受的内容,并具有正确的限制。

当然,所有 5 项都是对 foo(char*) 的改进,我遇到的比我愿意提及的要多。

【讨论】:

  • “必须在某个时候创建​​字符串”不,它不是,就像在 f(...) { cout
  • 您是在寻找答案还是争论?
  • 我的第一点是在某些时候必须创建一个结构来将字符串保存在内存中。如果您不希望它成为一个对象,请坚持使用 C。
  • 对于第二点,我想我依赖我的主观经验,我理解 (2) 要求比模板或迭代器范围签名更快。
猜你喜欢
  • 2013-10-31
  • 1970-01-01
  • 1970-01-01
  • 2014-12-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-01-16
  • 2015-12-09
相关资源
最近更新 更多