Search code examples
c++stringc++17variant

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


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.


Solution

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

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