Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to implement a DefaultIfNull function in C++?

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)) {
        // ...
    }
}
like image 691
Martin Ba Avatar asked Dec 03 '22 09:12

Martin Ba


2 Answers

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');
  }
}
like image 143
Ayxan Haqverdili Avatar answered Jan 04 '23 23:01

Ayxan Haqverdili


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.

like image 39
eerorika Avatar answered Jan 04 '23 23:01

eerorika