这两个函数为什么没有合并成一个函数?

3

在C++标签下最受欢迎的问题之一是名为"在C++中拆分字符串"。在这个问题中,提问者问道:"在C++中拆分字符串的最优雅的方式是什么?"。

回答此问题的得票最高的答案提供了以下两个函数:

std::vector<std::string> &split(const std::string &s, char delim, std::vector<std::string> &elems) {
    std::stringstream ss(s);
    std::string item;
    while (std::getline(ss, item, delim)) {
        elems.push_back(item);
    }
    return elems;
}


std::vector<std::string> split(const std::string &s, char delim) {
    std::vector<std::string> elems;
    return split(s, delim, elems);
}

这些函数很好用。但是我想知道为什么回答者没有将这两个函数合并成一个函数。当你将这些函数合并时,是否会有一些性能、可用性或易读性的优点,而你却忽略了呢?下面是结合后的完整程序:
#include <iostream>
#include <vector>
#include <string>
#include <sstream>

using namespace std;    
// splitting them into two seperate functions is unnecessary it seems to me, and makes the underlying function harder to understand.
std::vector<std::string> split(const std::string &s, char delim) {
    std::vector<std::string> elems;
    std::stringstream ss(s);
    std::string item;
    while (std::getline(ss, item, delim)) {
        elems.push_back(item);
    }
    return elems;
}

int main()
{
    std::vector<std::string> x = split("one:two::three", ':');
    for (int i = 0; i<x.size(); ++i) cout << x[i] << '\n';
    return 0;
}

我认为将这个函数分开来写显得不够优雅,而且更难理解,但我感觉可能是我漏掉了什么。为什么他不把它们合并起来呢?
2个回答

4
想象一下要拆分多个不同的来源,但希望结果都出现在一个容器中。为此,您不希望该函数总是为您分配新容器:
之前需要额外的工作:
std::vector<std::string> sources = /* populate */;

std::vector<std::string> results;

for (const auto& source : sources)
{
    auto result = split(source, ":");

    // or maintain a vector of vectors...yuck
    results.insert(std::make_move_iterator(result.begin()),
                   std::make_move_iterator(result.end()));
}

之后,简单明了:
std::vector<std::string> sources = /* populate */;

std::vector<std::string> results;

for (const auto& source : sources)
{
    split(source, ":", results);
}

或者说,假设你要拆分一堆不同的来源,并且为了提高效率,你想尽量减少内存分配(例如,你的分析器指出你在这里分配了太多内存)。因此,你可以反复重用同一个向量,以避免在第一次拆分后进行后续内存分配。

之前,速度很慢:

std::vector<std::string> sources = /* populate */;

for (const auto& source : sources)
{
    auto result = split(source, ":");

    process(result);
}

之后,更好:

std::vector<std::string> sources = /* populate */;

std::vector<std::string> result;
for (const auto& source : sources)
{
    result.clear();
    split(source, ":", result);

    process(result);
}

当然,在一般情况下,由计算机为您创建容器的简便性很好,我们可以轻松地重用更通用的函数来创建第二个函数,成本很小。

在您看来,如果API基于迭代器,那不是更好吗? - NPE
@NPE:您希望两个操作都基于迭代器吗?还是只有内部的操作? - David Rodríguez - dribeas
@NPE:实际上,我的第三次编辑本来要对此进行评论。但现在它已经在评论部分了,那就归到那里吧。 :) 是的,对于库代码来说,接受输出迭代器可能会更好些。但这取决于谁在使用它和谁在编写它。 - GManNickG
@GManNickG - 我觉得顶部函数应该有一个不同或更具描述性的名称(以及更具描述性的参数/参数)。也许像SplitAndMergeWithVector(&inputstring, delimeter, &outputvector)这样的东西。我肯定可以看到用这种方式做的好处。它绝对更强大-但我觉得它仍然不如组合版本“优雅”-无论这意味着什么。感谢您帮助我理解其功效/好处。 - Stepan1010

3
您从原始代码中复制的内容提供了两个略有不同语义的函数。第一个函数将拆分的结果添加到现有向量中,而第二个函数则在第一个函数的基础上创建一个只包含这个拆分元素的新向量。
通过提供两个函数,您可以以相同的成本提供两种不同的行为,以满足不同用户的不同需求。如果您将它们合并为单一版本,并且一个用户需要构建一个包含从多个字符串中拆分获得的所有标记的列表,则必须创建多个向量并将它们合并。
有趣的一点,不是关于设计一个/两个函数的问题,而是实际实现。第二个函数应实现为:
std::vector<std::string> split(const std::string &s, char delim) {
    std::vector<std::string> elems;
    split(s, delim, elems);
    return elems;

}

区别在于,不是从内部调用 split 返回获取的引用,而应直接返回本地变量。这种更改使本地变量的命名返回值优化成为可能,消除了复制的成本** 在 C++11 中,您还可以使用 return std::move(split(s,delim,elems)); 来获得相同的行为,但这需要更多的击键,并且仍然是一个move 而不是整个操作的移除。

感谢您的解释。请阅读此处的第一段 - 它非常好地解释了拥有两个单独函数的好处。 - Stepan1010

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