4

Disclaimer: This is rather more out of curiosity than for a lack of other solutions!

Is it possible to implement a function in C++ that:

  • gets passed a pointer of type T
  • either returns a reference-like-thing to the object pointed to by T
  • or, if the pointer is null, returns a reference-like-thing to a default constructed T() that has some sane lifetime?

Our first try was:

template<typename T>
T& DefaultIfNullDangling(T* ptr) {
    if (!ptr) {
        return T(); // xxx warning C4172: returning address of local variable or temporary
    } else {
        return *ptr;
    }
}

A second attempt was done like this:

template<typename T>
T& DefaultIfNull(T* ptr, T&& callSiteTemp = T()) {
    if (!ptr) {
        return callSiteTemp;
    } else {
        return *ptr;
    }
}

This gets rid of the warning and somewhat extends the lifetime of the temporary, but it's still rather error prone, I think.


Background:

The whole thing was triggered by an access pattern that looked like this:

if (pThing) {
  for (auto& subThing : pThing->subs1) {
    // ...
    if (subThing.pSubSub) {
      for (auto& subSubThing : *(subThing.pSubSub)) {
         // ...
      }
    }
  }
}

that could be "simplified" to:

for (auto& subThing : DefaultIfNull(pThing).subs1) {
    // ...
    for (auto& subSubThing : DefaultIfNull(subThing.pSubSub)) {
        // ...
    }
}
Martin Ba
  • 37,187
  • 33
  • 183
  • 337
  • You can return a pointer, and use `nullptr`. Or you can return `std::optional>` if you insist on passing a reference-like type. If you really really want to return a reference, you need some sort of global or static instance to refer to. You can't create an instance on the spot inside a function and return a reference to it. And this only really works if you return a const reference. You do not want to pass a non-const reference to a sentinel value as anyone could change it. – François Andrieux May 18 '21 at 13:37
  • I dont do c++, but doesnt `new` allocate memory that has to be deleted? That seems like a way – DownloadPizza May 18 '21 at 13:38
  • With `const`, you might have a `static const T dummy; return dummy;` (without const, the returned mutable instance would be shared :-/ so value would be unpredictable). – Jarod42 May 18 '21 at 13:39
  • @Jarod42 - yeah with const it'd be easier to use a static. Non const I dunno how to acheive this. – Martin Ba May 18 '21 at 13:41
  • @DownloadPizza `new` can allocate memory, but it usually does more harm than it helps. In this case the function cannot know if `ptr` points to something dynamically allocated, and returning a mix of owning / non-owning raw pointers from the same function is recipe for desaster. – 463035818_is_not_an_ai May 18 '21 at 13:42
  • 2
    One solution would be to implement a proxy range type containing a pointer. This type would provide the `begin` and `end` members which either forward the call to the pointed container or provide an empty range. The usage would be basically identical to using a `NullOrEmpty` function, in the context of a range-based for loop. – François Andrieux May 18 '21 at 13:42
  • 1
    Your post suggests you're iterating over a container of pointers to containers and you'd like to skip the `nullptrs` in a convenient way. Now, the question is: is the default (nullptr's case) going to be used in any other way than just for a clean dereference? If not, maybe using `boost::filter_iterator` is the way to go? True, you lose the ranged for loop, but still it might be worth it. – alagner May 18 '21 at 14:16
  • The fact that as of C++20, we don't have `std::map::get(key, default_value)` suggests that the answer is "no". – sbabbi May 18 '21 at 20:54

6 Answers6

6

There isn't really a good, idiomatic C++ solution that would exactly match what you're asking for.

A language where "EmptyIfNull" would work well, is probably one that has either garbage collection, or reference counted objects. So, we can achieve something similar in C++ by using reference counted pointers:

// never returns null, even if argument was null
std::shared_pr<T>
EmptyIfNull(std::shared_pr<T> ptr) {
    return ptr
        ? ptr
        : std::make_shared<T>();
}

Alternatively, you could return a reference to an object with static storage duration. However, I would not return a mutable reference when using such technique, since one caller might modify the object to be non-empty which might be highly confusing to another caller:

const T&
EmptyIfNull(T* ptr) {
    static T empty{};
    return ptr
        ? *ptr
        : empty;
}

Alternatively, you could still return a mutable reference, but document that not modifying the empty object is a requirement that the caller must obey. That would be brittle, but that's par for the course in C++.


As another alternative, I was writing a suggestion to use a type-erasing wrapper that is either a reference, or an object, but Ayxan Haqverdili has got it covered already. Tons of boilerplate though.


Some alternative designs that adjust the premise a bit more, to be suitable to C++:

Return an object:

T
EmptyIfNull(T* ptr) {
    return ptr
        ? *ptr
        : T{};
}

Let the caller provide the default:

T&
ValueOrDefault(T* ptr, T& default_) {
    return ptr
        ? *ptr
        : default_;
}

Treat a non-null argument as a pre-condition:

T&
JustIndirectThrough(T* ptr) {
    assert(ptr); // note that there may be better alternatives to the standard assert
    return *ptr;
}

Treat a null argument as an error case:

T&
JustIndirectThrough(T* ptr) {
    if (!ptr) {
        // note that there are alternative error handling mechanisms
        throw std::invalid_argument(
            "I can't deal with this :(");
    }
    return *ptr;
}

Background:

I don't think the function that you're asking for is very attractive for the background that you give. Currently, you do nothing if the pointer is null, while with this suggestion you would be doing something with an empty object. If you dislike the deeply nested block, you could use this alternative:

if (!pThing)
    continue; // or return, depending on context

for (auto& subThing : pThing->subs1) {
    if (!subThing.pSubSub)
        continue;

    for (auto& subSubThing : *subThing.pSubSub) {
       // ...
    }
}

Or, perhaps you could establish an invariant that you never store null in the range, in which case you never need to check for null.

eerorika
  • 232,697
  • 12
  • 197
  • 326
6

Yes, but it's going to be ugly:

#include <stdio.h>

#include <variant>

template <class T>
struct Proxy {
 private:
  std::variant<T*, T> m_data = nullptr;

 public:
  Proxy(T* p) {
    if (p)
      m_data = p;
    else
      m_data = T{};
  }

  T* operator->() {
    struct Visitor {
      T* operator()(T* t) { return t; }
      T* operator()(T& t) { return &t; }
    };

    return std::visit(Visitor{}, m_data);
  }
};

struct Thing1 {
  int pSubSub[3] = {};
  auto begin() const { return pSubSub; }
  auto end() const { return pSubSub + 3; }
};

struct Thing2 {
  Thing1* subs1[3] = {};
  auto begin() const { return subs1; }
  auto end() const { return subs1 + 3; }
};

template <class T>
auto NullOrDefault(T* p) {
  return Proxy<T>(p);
}

int main() {
  Thing1 a{1, 2, 3}, b{4, 5, 6};
  Thing2 c{&a, nullptr, &b};

  auto pThing = &c;

  for (auto& subThing : NullOrDefault(pThing)->subs1) {
    for (auto& subSubThing : NullOrDefault(subThing)->pSubSub) {
      printf("%d, ", subSubThing);
    }
    putchar('\n');
  }
}
Aykhan Hagverdili
  • 28,141
  • 6
  • 41
  • 93
4

Sadly, but no. There is really no way to fully achieve what you want. Your options are:

  • If passed pointer is nullptr, return a reference to static object. This would only be correct if you are returning a const reference, otherwise, you are exposing yourself to a huge can of worms;
  • Return an std::optional<std::ref> and return unset optional if pointer is nullptr. This doesn't really solve your problem, as you still have to check at the call site if the optional is set, and you might as well check for the pointer to be nullptr instead at the call site. Alternatively, you can use value_or to extract value from optional, which would be akin to next option in a different packaging;
  • Use your second attempt, but remove default argument. This will mandate call site to provide a default object - this makes code somewhat ugly
SergeyA
  • 61,605
  • 5
  • 78
  • 137
3

If you only want to skip over nullptrs easily, you could just use boost::filter_iterator. Now, this does not return default value on null pointer occurence, but neither does OP's original code; instead it wraps the container and provides the API to silently skip it in the for loop.

I skipped all the boilerplate code for brevity, hopefully the snippet below illustrates the idea well.

#include <iostream>
#include <memory>
#include <vector>
#include <boost/iterator/filter_iterator.hpp>
 
struct NonNull                                                                                                                                                                                
{           
    bool operator()(const auto& x) const { return x!=nullptr;}
};          
            
class NonNullVectorOfVectorsRef
{           
public:     
    NonNullVectorOfVectorsRef(std::vector<std::unique_ptr<std::vector<int>>>& target)
        : mUnderlying(target)
    {}      
            
    auto end() const
    {       
        return boost::make_filter_iterator<NonNull>(NonNull(), mUnderlying.end(), mUnderlying.end());
            
    }       
    auto begin() const
    {       
        return boost::make_filter_iterator<NonNull>(NonNull(), mUnderlying.begin(), mUnderlying.end());
    }       
private:    
    std::vector<std::unique_ptr<std::vector<int>>>& mUnderlying;
};          
            
int main(int, char*[])
{           
    auto vouter=std::vector<std::unique_ptr<std::vector<int>>> {}; 
    vouter.push_back(std::make_unique<std::vector<int>>(std::vector<int>{1,2,3,4,5}));
    vouter.push_back(nullptr);
    vouter.push_back(std::make_unique<std::vector<int>>(std::vector<int>{42}));
            
    auto nn = NonNullVectorOfVectorsRef(vouter);
    for (auto&& i:nn) {
        for (auto&& j:(*i)) std::cout << j <<  ' ';
        std::cout << '\n';
    }       
    return 0;
}   
alagner
  • 3,448
  • 1
  • 13
  • 25
2

If you accept std::shared_ptr<T>, you could use them to achieve this in a rather save and portable way:

template<typename T>
std::shared_ptr<T> NullOrDefault(std::shared_ptr<T> value)
{
    if(value != nullptr)
    {
        return value;
    }
    return std::make_shared<T>();
}
Detonar
  • 1,409
  • 6
  • 18
2

From the comments:

One solution would be to implement a proxy range type containing a pointer. This type would provide the begin and end members which either forward the call to the pointed container or provide an empty range. The usage would be basically identical to using a NullOrEmpty function, in the context of a range-based for loop. – François Andrieux yesterday

This is basically similar to what Ayxan provided in another answer, though this one here does work with exactly the client side syntax shown in the OP by providing begin() and end():

template<typename T>
struct CollectionProxy {
    T* ref_;
    // Note if T is a const-type you need to remove the const for the optional, otherwise it can't be reinitialized:
    std::optional<typename std::remove_const<T>::type> defObj;

    explicit CollectionProxy(T* ptr) 
    : ref_(ptr)
    {
        if (!ref_) {
            defObj = T();
            ref_ = &defObj.value();
        }
    }

    using beginT = decltype(ref_->begin());
    using endT = decltype(ref_->end());

    beginT begin() const {
        return ref_->begin();
    }
    endT end() const {
        return ref_->end();
    }
};

template<typename T>
CollectionProxy<T> DefaultIfNull(T* ptr) {
    return CollectionProxy<T>(ptr);
}

void fun(const std::vector<int>* vecPtr) {
    for (auto elem : DefaultIfNull(vecPtr)) {
        std::cout << elem;
    }
}

Notes:

  • Allowing for T and T const seems a wee bit tricky.
  • The solution using a variant would generate a smaller proxy object size (I think).
  • This is certainly gonna be more expensive at runtime than the if+for in the OP, after all you have to at least construct an (empty) temporary
    • I think providing an empty range could be done cheaper here if all you need is begin() and end(), but if this should generalize to more than just calls to begin() and end(), you would need a real temporary object of T anyways.
Martin Ba
  • 37,187
  • 33
  • 183
  • 337