user2546926 user2546926 - 1 month ago 9
C++ Question

how to return an reference to an 'empty' object

The problem I tried to tackle today is the one where you want to return a reference, but you actually can't because in some specific case you return something 'empty'. Just to clarify, something like this:

std::array<std::string, 5> cStrings /* do something to init */

const std::string& get_my_string(/* args */) {
if(i_know_what_to_return()) {
/*
* returning happily something in
* cStrings if I know what
*/
} else {
/* oeps... i need to return something, I don't have :-( */

return std::string(); // DON'T TRY THIS AT HOME!
}
}


I was wondering if there is not a generic approach to avoid the creation of empty objects all over the place, or, even worse, start returning copies of the objects. So I was wondering if I could not create some sort of template class that allows me to tell the compiler to manage this for me. This was my approach (although its only one of many, all with a small variation in the template declaration since this produces an error)

template<typename T, decltype(T) V>
struct empty_reference {
using type = T;
static const T static_object_;
operator T&() const {
return static_object_;
}
};

template<typename T, decltype(T) V>
const typename empty_reference<T, V>::type
empty_reference<T, V>::static_object_ = V;


unfortunately, this does not work (I get an error on 'decltype(T) V' saying 'decltype expects an expression not a type'), but I guess this is mainly because I am missing something in the template declaration.
In the end I am hoping to use this class by returning

return empty_reference<std::string, std::string()>();


So I have three questions here;


  1. could this be possible

  2. how do I make this work, what should I turn 'decltype(T) V' into to tell the compiler that V should be of type T while still being evaluated during compilation?

  3. and is this a good approach or is there an easier/better solution to this problem?


Answer

Not exactly the same, but you can get your string as a function parameter instead of as a return value and rely on the fact that temporaries bind to const references.
As an example:

#include <string>
#include <iostream>
#include <utility>

struct S {
    template<typename F>
    void get(bool b, F &&f) {
        std::forward<F>(f)(b ? s : "");
    }

    std::string s{"foo"};
};

int main() {
    S s;
    s.get(true, [](const auto &str) { std::cout << str << std::endl; });
    s.get(false, [](const auto &str) { std::cout << str << std::endl; });
}

This can be a valid alternative if libraries like Boost are not already part of your project and you don't want to include them.


Otherwise, as others have mentioned, you can pick up an upcoming utility called std::optional and combine it with std::reference_wrapper as it follows:

#include <string>
#include <iostream>
#include <experimental/optional>
#include <functional>

struct S {
    std::experimental::optional<std::reference_wrapper<std::string>> get(bool b) {
        return b ? std::ref(s) : std::experimental::optional<std::reference_wrapper<std::string>>{};
    }

    std::string s{"foo"};
};

int main() {
    S s;

    auto opt1 = s.get(true);
    std::cout << (opt1 ? opt1->get() : "-") << std::endl;

    auto opt2 = s.get(false);
    std::cout << (opt2 ? opt2->get() : "-") << std::endl;
}

Pretty ugly indeed. Note that a std::optional should be verified through its operator bool or the member method has_value to be sure that it contains a value.

Unfortunately you cannot use directly a std::reference_wrapper as return value, for it cannot be (let me say) _empty). In other terms, if you want to construct such an object, you must pass a valid reference to its constructor.


Another approach would be by using a template class like the following one:

#include <string>
#include <type_traits>
#include <iostream>

template<typename T>
struct defval {
    static const std::decay_t<T> value;
};

template<typename T>
const std::decay_t<T> defval<T>::value = T{};

struct S {  
    const std::string & get(bool b) {
        return b ? str : defval<std::string>::value;
    }

    std::string str{"foo"};
};

int main() {
    S s;
    std::cout << s.get(true) << std::endl;
    std::cout << s.get(false) << std::endl;
}

Note that you must specialize it for those types that are not default constructible.

Comments