【问题标题】:What are the best practices for designing function templates to deal with string/char arguments?设计函数模板以处理字符串/字符参数的最佳实践是什么?
【发布时间】:2018-05-27 06:42:31
【问题描述】:

我想写一个简单的字符串split函数。

该函数应采用一个std::basic_string 和一个分隔符(可能是CharT 或std::basic_string),并将结果放入ContainerT。

我的第一次尝试是

template <typename StringT, typename DelimiterT, typename ContainerT>
void split(
    const StringT &str, const DelimiterT &delimiters, ContainerT &conts) {
    conts.clear();
    std::size_t start = 0, end;
    std::size_t len = delimiters.size();
    while ((end = str.find(delimiters, start)) != StringT::npos) {
        if (end - start) {
            conts.emplace_back(str, start, end - start);
        }
        start = end + len;
    }

    if (start != StringT::npos && start < str.size()) {
        conts.emplace_back(str, start, str.size() - start);
    }
}

我的最终目标是扩展这个功能来实现:

  1. 最终结果总是std::basic_string&lt;CharT&gt; 放入一些conts。
  2. 第一个参数str 可以是std::basic_string&lt;CharT&gt;、const CharT* 或字符串文字。
  3. 第二个参数delimiter可以是char,也可以是std::basic_string&lt;CharT&gt;/const CharT*/字符串字面量,表示分隔符的长度大于1,例如将aaa,,bbb,c 与,, 分开得到aaa/bbb,c。
  4. 第三个参数可以是来自STL的任何序列容器。

由于 1 通常处理 C++ 中的现代字符串,因此 2 可能是 std::basic_string&lt;CharT&gt; 只是为了简化。

鉴于函数(模板)可以重载,我想知道

  1. 在这种情况下我至少需要多少个函数?
  2. 设计此类函数的最佳实践是什么(如何编写更通用的函数)?例如,也许要使上述函数与const CharT* 分隔符一起工作,必须将std::size_t len = delimiters.size(); 行更改为std::distance(...)?

更新:

添加了一个相似的代码审查here。

【问题讨论】:

  • 我觉得这很棘手,因为所有潜在的组合。我尝试了很多方法。我目前使用的有一个 master 模板函数,它只为输入字符串和定界符(四个迭代器 s_begin、s_end、d_begin、d_end)采用迭代器。然后我有很多不同的重载将它们的输入参数转换为最基本的形式。
  • 你看到stackoverflow.com/questions/236129/…的顶级答案了吗

标签: c++ c++17


【解决方案1】:

您可以使用std::string_view 来分割和分隔文本。此外,您可以使用模板模板参数来选择结果中的元素类型:

template<typename Char, template<typename> class Container, typename String>
Container<String> split_impl(std::basic_string_view<Char> text, std::basic_string_view<Char> delim)
{
    Container<String> result;
    //...
    result.push_back(String(text.substr(start, count)));
    //...
    return result;
}

template<template<typename> class Container, typename String = std::string_view>
Container<String> split(std::string_view text, std::string_view delim)
{ return split_impl<char, Container, String>(text, delim); }

template<template<typename> class Container, typename String = std::u16string_view>
Container<String> split(std::u16string_view text, std::u16string_view delim)
{ return split_impl<char16_t, Container, String>(text, delim); }

这样,它可以与std::string、std::string_view 和const char* 一起使用而无需冗余分配:

// vector of std::string_view objects:
auto words_1 = split<std::vector>("hello world", " ");

// list of std::string objects:
auto words_2 = split<std::list, std::string>(std::string("hello world"), " ");

// vector of std::u16string_view objects:
auto words_3 = split<std::vector>(u"hello world", u" ");

编辑:为char 和char16_t 添加重载

编辑 2

在上面的代码中,split_impl 确实有效。 split 重载仅用于简化用户代码,因此您不必显式指定要使用的字符类型。没有重载是必要的,因为当参数类型为 basic_string_view 并且您正在传递不同类型的参数(例如,const char* 或 std::wstring)时,编译器无法推断出 Char。总的来说,我认为这不是一个大问题 - 可能,您希望有四个重载(char、char16_t、char32_t、wchar_t),如果不是更少的话。

但是,为了完整起见,这里有一个不使用重载的替代方法:

template<typename ContainerT, typename TextT, typename DelimT>
ContainerT split(const TextT& text, const DelimT& delim)
{
    using CharT = std::remove_reference_t<decltype(text[0])>;

    std::basic_string_view<CharT> textView(text);
    std::basic_string_view<CharT> delimView(delim);

    ContainerT result;

    // actual implementation, but using textView and delimView instead of text and delim

   result.push_back(textView.substr(start, count));

   return result;
}

// usage:
auto words = split<std::vector<std::string_view>>("some text", " ");

使用这种方法,您不能使用String 模板参数的默认值,如上所述(因为它必须依赖于TextT 类型)。出于这个原因,我删除了它。此外,此代码假定text 和delim 使用相同的字符类型,并且可以转换为basic_string_view。

就个人而言,我更喜欢版本 1。它不使用模板类型作为函数参数,恕我直言更好,因为它让调用者更好地了解应该传入什么。换句话说,第一个 split 的接口更好地指定。另外,如上所述,我不认为必须添加四个 split 的重载是一个问题。

【讨论】:

  • 感谢您的回答。这个版本似乎不关心wstring/u16string/u32string。虽然string 可以隐式转换为string_view,但basic_string&lt;T&gt; 在进行模板参数推导时似乎无法转换basic_string_view&lt;T&gt;。
  • 我已更新答案以包括 char 和 char16_t 的重载。我认为这可以解决问题。
  • 是否有可能以某种方式合并这些char/wchar_t/char16_t...?
  • 用我能想到的一种方式再次更新。
【解决方案2】:

从输入字符串构造basic_string_view,然后对其进行操作。 basic_string_view 有一个采用char* 的显式构造函数,basic_string 有一个转换为basic_string_view 的运算符。

【讨论】:

  • 应该使用一些std::basic_string_view&lt;CharT&gt;作为模板函数参数吗?似乎如果传递 std::string 它不能被隐式转换,这在传递参数时似乎失去了一些便利。
  • std::string 有一个转换运算符用于转换为std::string_view,所以这应该不是问题。
【解决方案3】:

我的建议是使用两个模板参数,一个用于输入字符串,一个用于输出容器,因为在几乎所有情况下,输入字符串、分隔符和输出容器都是相同的类型,因此您可以像这样定义函数-

template<typename charT, typename Container)
void split(const std::basic_string<charT> input,
    const charT deliminator,
    Container<std::basic_string<chart>> &cont)

对于第二种情况,您的分隔符可以是 std::basic_string&lt;charT&gt; 类型。

【讨论】:

    猜你喜欢
    • 2017-12-01
    • 2011-01-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多