【问题标题】:returning a std::string from a variant which can hold std::string or double从可以保存 std::string 或 double 的变体返回 std::string
【发布时间】:2022-01-18 18:20:49
【问题描述】:

我有以下代码:

#include <variant>
#include <string>
#include <iostream>

using Variant = std::variant<double, std::string>;

// helper type for the visitor
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
// explicit deduction guide (not needed as of C++20)
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;


std::string string_from(const Variant& v)
{
    return std::visit(overloaded {
        [](const double arg) { return std::to_string(arg); },
        [](const std::string& arg) { return arg; },
        }, v);
}

int main()
{
    Variant v1 {"Hello"};
    Variant v2 {1.23};
    
    std::cout << string_from(v1) << '\n';
    std::cout << string_from(v2) << '\n';

    return 0;
}

我有一个名为string_from() 的函数,它接受一个变体并将其内部值转换为字符串。

变体可以包含std::string 或double。

如果是std::string,我就直接退货。

如果是double,我会从double 创建一个std::string,然后将其返回。

问题是,我不喜欢在字符串变体的情况下返回std::string 的副本。理想情况下,我会返回 std::string_view 或其他类型的字符串观察者。

但是,我无法返回 std::string_view,因为在双变量的情况下,我需要创建一个新的临时 std::string 并且 std::string_view 是非拥有的。

出于同样的原因,我无法返回 std::string&amp;。

我想知道是否有办法优化代码,以便在字符串变体的情况下避免复制。

请注意,在我的实际用例中,我经常从字符串变体中获取字符串,但很少从双变体中获取字符串。

但我仍然希望能够从双变量中获得std::string。

另外,在我的实际用例中,我通常只是观察字符串,所以我并不需要每次都复制。 std::string_view 或其他一些字符串观察器在这种情况下是完美的,但由于上述原因,这是不可能的。

我考虑了几种可能的解决方案,但我不喜欢其中任何一个:

  1. 返回char* 而不是std::string 并在double 的情况下将c 字符串分配到堆上的某处。在这种情况下,我还需要将整个内容包装在一个拥有堆分配字符串的类中以避免内存泄漏。

  2. 返回带有自定义删除器的std::unique_ptr&lt;std::string&gt;,该删除器将清理堆分配的字符串,但如果字符串驻留在变体中,则不会执行任何操作。不确定如何实现这个自定义删除器。

  3. 更改变体,使其包含std::shared_ptr&lt;std::string&gt;。然后,当我需要来自字符串变体的字符串时,我只返回一个 shared_ptr 的副本,当我需要来自双变体的字符串时,我调用 std::make_shared()。

第三种解决方案有一个固有的问题:std::string 不再驻留在变体中,这意味着追逐指针并失去性能。

您能否针对此问题提出任何其他解决方案?每次调用函数时都比复制std::string 性能更好。

【问题讨论】:

  • 你真的需要存储double吗?为什么不总是将double 转换为std::string 并使用std::string 而不是变体?
  • 因为我使用变体作为保持这两种值的通用接口。在某些情况下我需要双打,在某些情况下我需要字符串。它们存储在一个共享容器中,它们的顺序很重要,所以我不能使用 2 个单独的容器。
  • 你真的需要返回一个字符串,而不仅仅是“使用”它:template &lt;typename F&gt; void do_job_with_string(F f, const Variant&amp; v) { std::visit(overloaded { [&amp;](const double arg) { f(std::to_string(arg)); }, [&amp;](const std::string&amp; arg) { f(arg); } }, v);}
  • 请记住,创建和复制短字符串已经很便宜,所以如果您的大部分字符串都很短,那么应该没有问题。从技术上讲,您可以返回一个 variant&lt;string, string_view&gt; (yikes) 或返回一个包含 string 和 string_view 的类型,您可以在其中专门使用视图,知道随附的字符串可以在需要时充当后备存储(也是一种 yikes )。我很高兴错了,但我认为 C++ 中的条件生命周期没有真正干净直接的解决方案。
  • 返回variant&lt;string, string_view&gt; 可能是一个选项。

标签: c++ string c++17 variant


【解决方案1】:

您可以返回一个代理对象。 (这就像你的 unique_ptr 方法)

struct view_as_string{
    view_as_string(const std::variant<double, std::string>& v){
        auto s = std::get_if<std::string>(&v);
        if(s) ref = s;
        else temp = std::to_string(std::get<double>(v));
    }
    const std::string& data(){return ref?*ref:temp;}
    const std::string* ref = nullptr;
    std::string temp;
};

使用

int main()
{
    std::variant<double, std::string> v1 {"Hello"};
    std::variant<double, std::string> v2 {1.23};
    
    std::cout << view_as_string(v1).data() << '\n';
    
    view_as_string v2s(v2);
    std::cout << v2s.data() << '\n';
}

【讨论】:

    【解决方案2】:

    问题是,一个变体拥有不同的类型,但您正试图找到一种方法以一种类型来表示所有这些类型。字符串表示对于通用日志记录很有用,但它有您描述的缺点。

    对于变体,我不喜欢尝试将值合并为一个共同的事物,因为如果这很容易实现,那么首先就不需要变体了。

    我认为,更好的做法是尽可能延迟转换,并继续将其转发到其他使用该值的函数,或者转换并转发直到它被使用——而不是试图提取单个值并尝试使用它。

    一个相当通用的函数可能如下所示:

    template <typename Variant, typename Handler>
    auto with_string_view(Variant const & variant, Handler && handler) {
       return std::visit(overloaded{
           [&](auto const & obj) {
               using std::to_string;
               return handler(to_string(obj));
           },
           [&](std::string const & str) {return handler(str); },
           [&](std::string_view str) { return handler(str); },
           [&](char const * str) { return handler(str); }
       }, variant);
    }
    

    由于在通用版本中创建的临时文件比对处理程序的调用更有效,因此这是安全且高效的。它还展示了我发现对变体非常有用的“转发它”技术(以及一般访问,即使对于非变体也是如此。)

    另外,我没有显式转换为 string_view,但该函数可以添加处理程序接受字符串视图的要求(如果这有助于记录用法。)

    使用上面的辅助函数,你可以这样使用它:

    using V = std::variant<std::string, double>;
    
    V v1{4.567};
    V v2{"foo"};
    
    auto print = [](std::string_view sv) { std::cout << sv << "\n";};
    with_string_view(v1, print);
    with_string_view(v2, print);
    

    这是一个完整的示例,也扩展了一点:https://godbolt.org/z/n7KhEW7vY

    【讨论】:

    • 您的建议似乎是最好的,但是,我的应用程序是围绕从变量中获取值并将其传递给其他函数或返回它而构建的,因此需要对我的代码进行大量更改.这就是为什么我没有将其标记为可接受的解决方案,但我认为如果您还没有编写基础架构,它是最好的解决方案。
    【解决方案3】:

    如果线程安全不是问题,您可以在返回双精度值时简单地使用静态std::string 作为后备存储。然后你就可以返回一个std::string_view,例如:

    std::string_view string_from(const Variant& v)
    {
        static std::string buffer;
        return std::visit(overloaded {
            [&buffer](const double arg) -> std::string_view { buffer = std::to_string(arg); return buffer; },
            [](const std::string& arg) -> std::string_view { return arg; },
            }, v);
    }
    

    Online Demo

    【讨论】:

      【解决方案4】:

      我提出了我自己的解决方案,灵感来自 apple apple 的 view_as_string 类解决方案。 这里是:

      class owning_string_view : public std::string_view
      {
      public:
          explicit owning_string_view(const char* str) : std::string_view{str}, m_string_buffer{} {}
          explicit owning_string_view(const std::string& str) : std::string_view{str}, m_string_buffer{} {}
          explicit owning_string_view(std::string&& str) : std::string_view{}, m_string_buffer{std::move(str)}
          {
              static_cast<std::string_view&>(*this) = m_string_buffer;
          }
      
      private:
          std::string m_string_buffer;
      };
      

      我没有采用Variant,而是让它更通用,而是采用字符串。 对于左值字符串,它只创建字符串的std::string_view。 对于右值字符串,它将字符串移动到缓冲区中。

      它从std::string_view 扩展而来,因此可以在std::string_view 上下文中无缝使用。

      当然,在创建右值owning_string_view 时,您必须小心不要从对象中切掉std::string_view 部分,但std::string_view 也是如此。您必须小心不要从右值 std::string 中获取 std::string_view。

      将owning_string_view 作为std::string_view 参数传递给函数是安全的,原因与将右值std::string 作为std::string_view 参数传递给函数是安全的原因相同。右值在函数调用期间存在。

      当我从Variantclass 返回一个 string_view 时,我也意识到了一个更深层次的问题。 如果我尝试从右值Variant 中提取std::string_view 或owning_string_view,我仍然会得到一个悬空的string_view,所以我添加了2 个函数来从Variant 中获取字符串:

      • 一个只接受左值变体,它返回owning_string_view。
      • 另一个只接受右值变体,它返回一个std::string,它从变体中移出(因为变体是右值)。

      另一个观察:理想情况下,我会创建owning_string_view constexpr 的前两个构造函数,但我不能,因为std::string 的默认构造函数不是constexpr。我希望这在未来有所改变。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2021-02-24
        • 1970-01-01
        • 1970-01-01
        • 2022-06-29
        • 1970-01-01
        • 2011-03-26
        • 1970-01-01
        相关资源
        最近更新 更多