是否可以在C++中实现DefaultIfNull函数
Is it possible to implement a DefaultIfNull function in C++?
免责声明:这更多是出于好奇,而不是因为缺乏其他解决方案!
是否可以在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;
}
}
注:
- 允许
T
和T const
似乎有点棘手 - 使用变体的解决方案将生成较小的代理对象大小(我认为)
- 这在运行时肯定会比OP中的
if
+for
更昂贵,毕竟您至少需要构造一个(空的)临时- 我认为,如果你只需要begin()和end(),那么在这里提供一个空范围可能会更便宜,但如果这应该推广到不仅仅是对begin(和end)的调用,那么你无论如何都需要一个真正的临时对象
T
- 我认为,如果你只需要begin()和end(),那么在这里提供一个空范围可能会更便宜,但如果这应该推广到不仅仅是对begin(和end)的调用,那么你无论如何都需要一个真正的临时对象
- 如果没有malloc,链表实现将失败
- 如何在c++中实现处理器调度模拟器
- 如何在c++中使用引用实现类似python的行为
- 实现无开销push_back的最佳方法是什么
- 使用简单类型列表实现的指数编译时间.为什么
- 如何在BST的这个简单递归实现中消除警告
- 实现一个在集合上迭代的模板函数
- 我应该实现右值推送功能吗?我应该使用std::move吗
- 如何正确实现和访问运算符的各种自定义枚举器
- C++Union/Struct位域的实现和可移植性
- 这个极客对极客的trie实现是否存在内存泄漏问题
- 在c++中实现LinkedList时,应出现未处理的错误
- 为左值和右值的包装器实现C++范围
- 使用模板进行堆栈实现; "name followed by :: must be a class or namespace"
- 使用GSoap实现ONVIF
- 在用于格式4的arm模拟器中实现功能时的一个问题
- 用于AVX的ln(x)的实现,m256
- 用常见虚拟函数实现的任意组合来实现派生类的正确方法是什么
- 在C++中,如何在类和函数(可能是模板化的)的头中编写完整的实现
- std::random_device是如何实现的