user2546926
user2546926

Reputation: 415

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?

Upvotes: 4

Views: 5226

Answers (3)

user2546926
user2546926

Reputation: 415

So at some point I figured it out, at least I guess.

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

template<typename T>
const typename empty_reference<T>::type
    empty_reference<T>::static_object_ = {};

The concept is that the compiler will 'generate' one static 'static_object_' member for all types you use it for. Therefore, for any type you used the empty_reference for, a static object is generated in your application and returned when they are requested (i.e. if you return empty_reference<int> in two different files, a reference to the same int value will be returned).

The key was to remove the 'decltype(T) V' completely and use the universal initialization (i.e. {}) to make sure that any 'empty' constructor is called to initialize the object.

The downside is the reference needs to be constant (otherwise you can edit your 'empty reference') and the type is required to be constructable using an empty constructor.

So I guess the only question remaining is whether it is a good idea (I am not going to just my own ideas). Happy to receive any other suggestions/feedback :-)

Upvotes: -1

skypjack
skypjack

Reputation: 50550

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.

Upvotes: 2

References must be bound to an object. So you can't return a reference in your case. Your options are:

  1. Return a (non-owning) pointer.
  2. Return something like boost::optional, which can hold a reference (unlike the soon to be added std::optional).

Upvotes: 0

Related Questions