【问题标题】:How to best define/construct/initialize a std::string wrapper class如何最好地定义/构造/初始化 std::string 包装类
【发布时间】:2020-08-17 17:25:15
【问题描述】:

我在 std::string 周围使用了一个包装类,但是初始化/构造它的最简单/最干净/最完整的方法是什么。我至少需要 3 种方法

  • 来自字符串文字
  • 从 std::string 右值,避免复制!?
  • 来自 string_view 和 string(是的,复制)

天真的程序员只想将任何构造自动委托给 std::string,但这不是一个特性。

struct SimpleString
{
    SimpleString() = default;
    
    template<typename T>
    SimpleString( T t ) : Text( t ) { }   // <==== experimental
    
    // Alternative: are these OK
    SimpleString( const char* text ) : Text( text ) { }
    SimpleString( std::string&& text ) : Text( text ) { }
    SimpleString( const std::string_view text ) : Text( text ) { }

    std::string Text;
};

先发制人:是的,我想要它,我需要它。用例:调用一个泛型函数,其中 SimpleString 的处理方式与 std::string 不同。

关于从 std::string 继承的注意事项:可能是个坏主意,因为一有机会就会发生隐式转换。

【问题讨论】:

  • 为什么需要std::string 包装器?用例是什么?它应该提供哪些std::string 没有的功能?如果你只需要给它添加一点功能,你最好只继承它。
  • @J.Schultke 你读过抢注吗?
  • 我会立即使用该派生类变体,但认为它不受欢迎(无论出于何种原因)。应该没问题,如果我的课程只是一个包装器,什么都不添加!?如果我想添加一个标志怎么办?啊,皱眉的原因是隐式转换到 std::string,我需要避免。
  • Josuttis 有一个非常有趣的演讲,谈到了一个类似的案例和move 的问题在这里:youtu.be/PNRju6_yn3o?t=233

标签: c++ string wrapper


【解决方案1】:

如果您的主要目标是字符串转换构造函数,那么您无需费力。最简单和最好的方法是只接受 API 上的 std::string 按值并将 std::move 就位。

class SimpleString
{
public:
    SimpleString() = default;
    SimpleString(std::string text) : Text(std::move(text)) { }
    // ... other constructors / assignment ...

private:
    std::string Text;
};

这样做,我们可以利用std::string 已经可以通过以下方式构造的事实:

  • C 字符串/文字,
  • std::string_view,
  • 其他std::string 对象(左值或右值),以及
  • 任何可能已经可以转换为std::string 的用户定义类型

这样做还让用户现在可以选择是通过移动当前的std::string 对象还是允许SimpleString 显式复制它来构建SimpleString。这为调用者提供了更大的灵活性。

这很有效,因为无论如何您已经拥有了这个std::string,因此按值接受它并std::moveing 对象是“坐下”这个值的简单方法。移动std::string 相当便宜,相当于对指针、大小和分配器进行了一些分配——因此这产生的性能开销可以忽略不计,同时也是一种简单且可维护的方法。


现代 C++ 和移动语义使得接受这些类型变得如此容易。您绝对可以去重载所有 N 种构造它的方法(const char*std::string_viewTs 可转换为字符串等);但是直接接受类型并移动它会简单得多。这是最灵活的方法,同时也保持简单和可维护。


Here are some benchmarks 比较使用模板与使用 std::string + std::move。一般来说,保持简单会更有利。你的同事和未来的维护者会感谢你(即使未来的维护者是你,一年后)。

【讨论】:

  • 现代 C++ 和移动语义 - 你的意思是 legacy C++ 移动语义? ;)
  • @J.Schultke 关于标准库的争论没有实际意义。它处理未知类型,并且无法决定调用此类型的移动构造函数是否(几乎)无操作。正因为如此,它通常必须加长。在这种情况下,我们知道类型,并且我们知道 std::string 的移动构造函数非常便宜。最后,但不是列表,在标准库中,您将永远找到(至少我没有见过)像这样的贪婪模板构造函数,这正是我们试图解释的原因。跨度>
  • 我添加了some benchmarks。在字符串构造方面,这两种解决方案在实践中产生了几乎相同的性能。
  • @J.Schultke OP 没有声明他需要所有底层构造函数都可以调用。按值解决方案允许用户简单地调用std::string{n, c} 作为SimpleStringWrapper 的参数,它仍然具有实际上相同的性能特征——这比复杂的模板更易于维护
  • 我展示了活跃的基准,而不是破坏。这甚至没有意义;你不能在不构造对象的情况下破坏它。基准链接记录了循环中任何内容的时间,并进行了大规模测试。由于DoNotOptimize,结果未优化。你向我展示了组装,而不是基准。您应该阅读 Google Bench ——它是了解真正基准测试的非常酷的工具。
【解决方案2】:

std::string 有很多构造函数,在你的类中重新实现它们会很麻烦。这就是为什么你最适合像这样转发给std::string 的构造函数的原因:

#include <utility>

struct SimpleString
{
    template <typename... Args>
    SimpleString(Args &&... args) : Text(std::forward<Args>(args)...)
    {
    }

    std::string Text;
};

为避免与默认构造函数和复制/移动构造函数发生冲突,我们将需要使用 SFINAE:

template <typename... Args>
constexpr bool is_empty_pack_v = sizeof...(Args) == 0;

template <typename T, typename... Args>
constexpr bool is_copy_or_move_pack_v = sizeof...(Args) == 1 &&
                                                   (... && std::is_same_v<T, std::remove_reference_t<Args>>);

struct SimpleString
{
    SimpleString() = default;
    SimpleString(const SimpleString &) = default;
    SimpleString(SimpleString &&) = default;

    template <typename... Args, std::enable_if_t<
        not is_empty_pack_v<Args...> &&
        not is_copy_or_move_pack_v<SimpleString, Args...> &&
        std::is_constructible<std::string, Args...>, int> = 0>
    SimpleString(Args &&... args) : Text(std::forward<Args>(args)...)
    {
    }

    std::string Text;
};

【讨论】:

猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-09-12
  • 1970-01-01
  • 1970-01-01
  • 2016-05-03
  • 2016-02-16
  • 2013-10-14
相关资源
最近更新 更多