Home > Software engineering >  returning a std::string from a variant which can hold std::string or double
returning a std::string from a variant which can hold std::string or double

Time:12-16

I have the following code:

#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;
}

I have a function called string_from() which takes a variant and converts its inner value to a string.

The variant can hold either a std::string or a double.

In case of a std::string, I just return it.

In case of a double, I create a std::string from the double and then return it.

The problem is, I don't like the fact that I'm returning a copy of the std::string in case of a string-variant. Ideally, I would return a std::string_view or another kind of string observer.

However, I cannot return a std::string_view because in case of a double-variant I need to create a new temporary std::string and std::string_view is non-owning.

I cannot return a std::string& for the same reason.

I'm wondering if there's a way to optimize the code so that I can avoid the copy in case of a string-variant.

Note in my actual use case, I obtain strings from string-variants very frequently, but very rarely from double-variants.

But I still want to be able to obtain a std::string from a double-variant.

Also, in my actual use case, I usually just observe the string, so I don't really need the copy every time. std::string_view or some other string-observer would be perfect in this case, but it is impossible due to the reasons above.

I've considered several possible solutions, but I don't like any of them:

  1. return a char* instead of a std::string and allocate the c-string somewhere on the heap in case of a double. In this case, I would also need to wrap the whole thing in a class which owns the heap-allocated strings to avoid memory leaks.

  2. return a std::unique_ptr<std::string> with a custom deleter which would cleanup the heap-allocated strings, but would do nothing in case the string resides in the variant. Not sure how this custom deleter would be implemented.

  3. Change the variant so it holds a std::shared_ptr<std::string> instead. Then when I need a string from the string-variant I just return a copy of the shared_ptr and when I need a string from the double-variant I call std::make_shared().

The third solution has an inherent problem: the std::string no longer resides in the variant, which means chasing pointers and losing performance.

Can you propose any other solutions to this problem? Something which performs better than copying a std::string every time I call the function.

CodePudding user response:

You can return a proxy object. (this is like your unique_ptr method)

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

Use

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';
    std::cout << view_as_string(v2).data() << '\n';

    return 0;
}

CodePudding user response:

The problem is, a variant holds different types, but you're trying to find a way to represent all of them in a single type. A string representation is useful for generic logging, but it has the downsides you describe.

For variants, I don't like trying to consolidate the values back into a single common thing, because if that was easily possible then there would be no need for the variant in the first place.

Better, I think, is to defer the conversion as late as possible, and keep forwarding it on to other functions that make use of the value as it is, or convert and forward until it's used--rather than trying to extract a single value and trying to use that.

A fairly generic function might look like this:

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);
}

Since the temporary created in the generic version outlives the call to the handler, this is safe and efficient. It also shows the "forward it on" technique that I've found to be very useful with variants (and visiting in general, even for non-variants.)

Also, I don't explicitly convert to string_view, but the function could add requirements that the handler accepts string views (if that helps document the usage.)

With the above helper function you might use it like this:

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);

Here's a full live example, expanded out a little too: https://godbolt.org/z/n7KhEW7vY

  • Related