从可以容纳 std::string 或 double 的变体返回 std::string

returning a std::string from a variant which can hold std::string or double

我有以下代码:

#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::stringdouble

如果是 std::string,我只是 return 它。

如果是 double,我从 double 创建一个 std::string,然后 return 它。

问题是,我不喜欢这样的事实,即在字符串变体的情况下,我正在 return 复制 std::string。理想情况下,我会 return 一个 std::string_view 或另一种字符串观察器。

但是,我不能 return std::string_view 因为如果是双变体,我需要创建一个新的临时 std::stringstd::string_view 是非拥有的.

出于同样的原因,我不能 return std::string&

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

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

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

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

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

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

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

  3. 更改变体,使其包含 std::shared_ptr<std::string>。然后,当我需要来自字符串变体的字符串时,我只是 return shared_ptr 的副本,当我需要来自双变体的字符串时,我调用 std::make_shared().

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

你能提出任何其他解决这个问题的方法吗?比我每次调用函数时复制 std::string 更好的东西。

您可以return一个代理对象。 (这就像你的 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';
}

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

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

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

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

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

如果线程安全不是问题,您可以简单地使用静态 std::string 作为 returning 双精度值时的后备存储。那么你就可以return一个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

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

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_viewowning_string_view 我仍然会以悬空 string_view 结束,所以我添加了 2 个函数来自 Variant:

的字符串
  • 一个只接受左值变体并且它 returns owning_string_view.
  • 另一个只接受右值变体,它 returns 一个 std::string,它是从变体中移出的(因为变体是右值)。

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