Можно ли реализовать функцию DefaultIfNull на С++?

Отказ от ответственности: это скорее из любопытства, чем из-за отсутствия других решений!

Можно ли реализовать на C++ функцию, которая:

  • получает указатель типа T
  • либо возвращает ссылку на объект, на который указывает T
  • или, если указатель нулевой, возвращает объект, похожий на ссылку, на сконструированный по умолчанию объект T() с разумным временем жизни?

Наша первая попытка была:

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

Вторая попытка была сделана так:

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

Это избавляет от предупреждения и несколько продлевает срок службы временного объекта, но я думаю, что он по-прежнему подвержен ошибкам.


Задний план:

Все это было вызвано шаблоном доступа, который выглядел следующим образом:

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

который можно упростить до:

for (auto& subThing : DefaultIfNull(pThing).subs1) {
    // ...
    for (auto& subSubThing : DefaultIfNull(subThing.pSubSub)) {
        // ...
    }
}

person Martin Ba    schedule 18.05.2021    source источник
comment
Вы можете вернуть указатель и использовать nullptr. Или вы можете вернуть std::optional<std::reference_wrapper<T>>, если вы настаиваете на передаче ссылочного типа. Если вы действительно хотите вернуть ссылку, вам нужен какой-то глобальный или статический экземпляр для ссылки. Вы не можете создать экземпляр на месте внутри функции и вернуть ссылку на него. И это действительно работает, только если вы возвращаете константную ссылку. Вы не хотите передавать неконстантную ссылку на дозорное значение, так как любой может ее изменить.   -  person François Andrieux    schedule 18.05.2021
comment
Я не занимаюсь С++, но разве new не выделяется память, которую нужно удалить? Это похоже на способ   -  person DownloadPizza    schedule 18.05.2021
comment
С const у вас может быть static const T dummy; return dummy; (без const возвращаемый изменяемый экземпляр будет общим :-/, поэтому значение будет непредсказуемым).   -  person Jarod42    schedule 18.05.2021
comment
@ Jarod42 - да, с константой было бы проще использовать статику. Non const Я не знаю, как этого добиться.   -  person Martin Ba    schedule 18.05.2021
comment
@DownloadPizza new может выделять память, но обычно это больше вредит, чем помогает. В этом случае функция не может знать, указывает ли ptr на что-то динамически выделенное, и возврат комбинации необработанных указателей владения/невладения из одной и той же функции является рецептом катастрофы.   -  person 463035818_is_not_a_number    schedule 18.05.2021
comment
Одним из решений может быть реализация типа прокси-диапазона, содержащего указатель. Этот тип предоставит элементы begin и end, которые либо перенаправят вызов в указанный контейнер, либо предоставят пустой диапазон. Использование будет в основном идентично использованию функции NullOrEmpty в контексте цикла for на основе диапазона.   -  person François Andrieux    schedule 18.05.2021
comment
В вашем сообщении предполагается, что вы перебираете контейнер указателей на контейнеры, и вы хотели бы пропустить nullptrs удобным способом. Теперь возникает вопрос: будет ли значение по умолчанию (случай nullptr) использоваться каким-либо другим способом, кроме как для чистого разыменования? Если нет, может быть, лучше использовать boost::filter_iterator? Правда, вы теряете диапазонный цикл for, но все же это может стоить того.   -  person alagner    schedule 18.05.2021
comment
Тот факт, что в C++20 у нас нет std::map::get(key, default_value), предполагает, что ответ отрицательный.   -  person sbabbi    schedule 18.05.2021


Ответы (6)


Да, но это будет некрасиво:

#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');
  }
}
person Ayxan Haqverdili    schedule 18.05.2021

На самом деле не существует хорошего идиоматического решения на С++, которое точно соответствовало бы тому, что вы просите.

Язык, в котором EmptyIfNull будет работать хорошо, вероятно, имеет либо сборку мусора, либо объекты с подсчетом ссылок. Итак, мы можем добиться чего-то подобного в C++, используя указатели с подсчетом ссылок:

// 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>();
}

В качестве альтернативы вы можете вернуть ссылку на объект со статической продолжительностью хранения. Однако я бы не стал возвращать изменяемую ссылку при использовании такой техники, поскольку один вызывающий объект может изменить объект, сделав его непустым, что может сильно сбить с толку другого вызывающего:

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

В качестве альтернативы вы все равно можете вернуть изменяемую ссылку, но задокументируйте, что не изменять пустой объект является требованием, которому должен подчиняться вызывающий. Это было бы хрупко, но это нормально для курса C++.


В качестве другой альтернативы я писал предложение использовать оболочку для стирания типов, которая является либо ссылкой, либо объектом, но Ayxan Haqverdili уже покрыл это. Тонны шаблонов, хотя.


Некоторые альтернативные проекты, которые немного корректируют предпосылку, чтобы они подходили для C++:

Вернуть объект:

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

Пусть вызывающая сторона предоставит значение по умолчанию:

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

Рассматривайте ненулевой аргумент как предварительное условие:

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

Рассматривайте нулевой аргумент как случай ошибки:

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;
}

Задний план:

Я не думаю, что функция, о которой вы просите, очень привлекательна для фона, который вы даете. В настоящее время вы ничего не делаете, если указатель равен нулю, в то время как с этим предложением вы будете делать что-то с пустым объектом. Если вам не нравится глубоко вложенный блок, вы можете использовать эту альтернативу:

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

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

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

Или, возможно, вы могли бы установить инвариант, что вы никогда не сохраняете null в диапазоне, и в этом случае вам никогда не нужно проверять наличие null.

person eerorika    schedule 18.05.2021

Печально, но нет. На самом деле нет никакого способа полностью достичь того, чего вы хотите. Ваши варианты:

  • Если переданный указатель равен nullptr, вернуть ссылку на статический объект. Это было бы правильно, только если вы возвращаете ссылку const, в противном случае вы подвергаете себя огромной банке червей;
  • Возвращает std::optional<std::ref> и необязательно возвращает значение unset, если указатель равен nullptr. На самом деле это не решает вашу проблему, так как вам все равно нужно проверить на сайте вызова, установлен ли optional, и вы могли бы также проверить, что указатель равен nullptr, а не на сайте вызова. В качестве альтернативы вы можете использовать value_or для извлечения значения из опционального, что было бы похоже на следующий параметр в другом пакете;
  • Используйте вторую попытку, но удалите аргумент по умолчанию. Это заставит сайт вызова предоставить объект по умолчанию - это делает код несколько уродливым.
person SergeyA    schedule 18.05.2021
comment
optional имеет value_or, что делает его совместимым с вариантом 3. - person Jarod42; 18.05.2021
comment
@ Jarod42 хорошая мысль, добавлю! - person SergeyA; 18.05.2021

Если вы хотите просто пропустить nullptrs, вы можете просто использовать boost::filter_iterator. Теперь это не возвращает значение по умолчанию при появлении нулевого указателя, как и исходный код OP; вместо этого он упаковывает контейнер и предоставляет API для его молчаливого пропуска в цикле for.

Я пропустил весь шаблонный код для краткости, надеюсь, приведенный ниже фрагмент хорошо иллюстрирует идею.

#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;
}   
person alagner    schedule 18.05.2021

Если вы принимаете std::shared_ptr<T>, вы можете использовать их для достижения этой и портативный способ:

template<typename T>
std::shared_ptr<T> NullOrDefault(std::shared_ptr<T> value)
{
    if(value != nullptr)
    {
        return value;
    }
    return std::make_shared<T>();
}
person Detonar    schedule 18.05.2021

Из комментариев:

Одним из решений может быть реализация типа прокси-диапазона, содержащего указатель. Этот тип будет предоставлять начальный и конечный элементы, которые либо перенаправляют вызов в указанный контейнер, либо предоставляют пустой диапазон. Использование будет в основном идентично использованию функции NullOrEmpty в контексте цикла for на основе диапазона. - Франсуа Андрие вчера

Это в основном похоже на то, что Ayxan предоставил в другом ответе, хотя этот здесь работает именно с синтаксисом на стороне клиента, показанным в OP, предоставив begin() и 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;
    }
}

Примечания:

  • Учет T и T const кажется немного сложным.
  • Решение, использующее вариант, сгенерирует меньший размер прокси-объекта (я думаю).
  • 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.
person Martin Ba    schedule 19.05.2021