是否可以在C++中实现DefaultIfNull函数

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

本文关键字:实现 DefaultIfNull 函数 C++ 是否      更新时间:2024-09-30

免责声明:这更多是出于好奇,而不是因为缺乏其他解决方案!

是否可以在C++中实现一个函数

  • 得到一个类型为T的指针
  • 返回一个类似于T所指向对象的引用
  • 或者,如果指针为null,则返回一个类似于引用的东西到默认构造的具有一定正常生存期的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)) {
// ...
}
}

是的,但它会很难看:

#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');
}
}

没有一个真正好的、惯用的C++解决方案能够完全满足您的要求。

一种语言,其中";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 Haqverdii已经涵盖了它。尽管有很多样板。


一些对前提进行更多调整的替代设计,以适合C++:

返回对象:

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

让调用者提供默认值:

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

将非null参数视为先决条件:

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

背景:

我不认为你所要求的函数对你所提供的背景很有吸引力。目前,如果指针为null,则不执行任何操作,而根据此建议,您将使用空对象执行某些操作。如果你不喜欢深度嵌套的块,你可以使用这个替代方案:

if (!pThing)
continue; // or return, depending on context
for (auto& subThing : pThing->subs1) {
if (!subThing.pSubSub)
continue;
for (auto& subSubThing : *subThing.pSubSub) {
// ...
}
}

或者,也许你可以建立一个不变量,你永远不会在范围中存储null,在这种情况下,你永远不需要检查null。

很遗憾,但没有。真的没有办法完全实现你想要的。您的选择是:

  • 如果传递的指针为nullptr,则返回对静态对象的引用。只有当您返回const引用时,这才是正确的,否则,您将暴露在大量蠕虫中
  • 返回一个std::optional<std::ref>,如果指针是nullptr,则返回未设置的可选值。这并不能真正解决您的问题,因为如果设置了optional,您仍然需要在调用站点进行检查,并且您还可以在调用站点检查指针是否为nullptr。或者,您可以使用value_or从optional中提取值,这类似于不同包装中的下一个option
  • 使用第二次尝试,但删除默认参数。这将强制调用站点提供一个默认对象——这使得代码有些难看

如果您只想轻松跳过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;
}   

如果您接受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>();
}

来自注释:

一种解决方案是实现一个包含指针。这种类型将提供开始和结束成员将调用转发到指向的容器,或者提供一个空的范围其用法与使用NullOrEmpty基本相同函数,在基于范围的for循环的上下文中。–弗朗索瓦Andrieux昨天

这基本上类似于Ayxan在另一个答案中提供的内容,尽管这个答案通过提供begin()end():与OP中显示的客户端语法完全一致

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

注:

  • 允许TT const似乎有点棘手
  • 使用变体的解决方案将生成较小的代理对象大小(我认为)
  • 这在运行时肯定会比OP中的if+for更昂贵,毕竟您至少需要构造一个(空的)临时
    • 我认为,如果你只需要begin()和end(),那么在这里提供一个空范围可能会更便宜,但如果这应该推广到不仅仅是对begin(和end)的调用,那么你无论如何都需要一个真正的临时对象T