c++gccparameter-passingc++17return-by-reference

Is there a way to return either a new object or reference to existing object from a function?


I am trying to write a function, which can either return a reference to an existing object passed as a first argument (if it is in correct state) or create and return a new object using literal passed as a second argument (default).

It would be even better if a function could take not only literal, but also another existing object as a second (default) argument and return a reference to it.

Below is a trivial implementation, but it does a lot of unneeded work:

  1. If called with lvalue as a second (default) argument, it calls a copy constructor of an argument, that is selected for return. Ideally a reference to an object should be returned.

  2. If called with literal as a second (default) argument, it calls constructor, copy constructor and destructor, even if second (default) argument is not selected for return. It would be better if an object is constructed and returned as rvalue reference without calling copy constructor or destructor.

std::string get_or_default(const std::string& st, const std::string& default_st) {
    if (st.empty()) return default_st
    else return st;
}

Is there a way to accomplish this more efficiently, while still keeping simple for caller? If I am correct, this requires a function to change return type based on run-time decision made inside a function, but I cannot think of a simple solution for caller.


Solution

  • I'm not 100% sure I understood the combinations of requirements but:

    #include <iostream>
    #include <string>
    #include <type_traits>
    
    // if called with an rvalue (xvalue) as 2:nd arg, move or copy
    std::string get_or_default(const std::string& st, std::string&& default_st) {
        std::cout << "got temporary\n";
        if(st.empty())
            return std::move(default_st);      // rval, move ctor
            // return std::forward<std::string>(default_st); // alternative
        else
            return st;                         // lval, copy ctor
    }
    
    // lvalue as 2:nd argument, return the reference as-is
    const std::string& get_or_default(const std::string& st,
                                      const std::string& default_st) {
        std::cout << "got ref\n";
        if(st.empty()) return default_st;
        else           return st;
    }
    
    int main() {
        std::string lval = "lval";
    
        // get ref or copy ...
        decltype(auto) s1 = get_or_default("", "temporary1");
        decltype(auto) s2 = get_or_default("", std::string("temporary2"));
        decltype(auto) s3 = get_or_default("", lval);
    
        std::cout << std::boolalpha;
        std::cout << std::is_reference_v<decltype(s1)> << "\n";
        std::cout << std::is_reference_v<decltype(s2)> << "\n";
        std::cout << std::is_reference_v<decltype(s3)> << "\n";
    }
    

    Output:

    got temporary
    got temporary
    got ref
    false
    false
    true
    

    Edit: Made a slightly more generic version after OP:s testing. It can use a lambda, like
    auto empty_check = [](const std::string& s) { return s.empty(); };
    to test if the first argument is empty.

    template<typename T, typename F>
    T get_or_default(const T& st, T&& default_st, F empty) {
        if(empty(st)) return std::move(default_st);
                   // return std::forward<T>(default_st); // alternative
        else          return st;
    }
    
    template<typename T, typename F>
    const T& get_or_default(const T& st, const T& default_st, F empty) {
        if(empty(st)) return default_st;
        else          return st;
    }