在C++中实现DefaultIfNull函数是否有可能?

4

免责声明:这只是出于好奇而非缺乏其他解决方案!

是否有可能在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)) {
        // ...
    }
}

你可以返回一个指针,并使用 nullptr。或者,如果您坚持要传递类似引用的类型,则可以返回 std::optional<std::reference_wrapper<T>>。如果您真的非常想返回一个引用,您需要一些全局或静态实例来引用它。您不能在函数内部即时创建实例并返回对其的引用。而且,这只有在您返回一个const引用时才能正常工作。您不希望将非const引用传递给哨兵值,因为任何人都可以更改它。 - François Andrieux
@Jarod42 - 是的,使用const会更容易使用静态变量。对于非const的情况,我不知道如何实现这一点。 - Martin Ba
@DownloadPizza new 可以分配内存,但通常会带来更多的伤害而不是帮助。在这种情况下,函数无法知道 ptr 是否指向动态分配的内容,并且从同一函数返回拥有/非拥有原始指针的混合是灾难的配方。 - 463035818_is_not_a_number
2
一个解决方案是实现一个包含指针的代理范围类型。该类型将提供 beginend 成员,这些成员将转发调用到指向的容器或提供一个空范围。在基于范围的 for 循环的上下文中,使用方式基本相同于使用 NullOrEmpty 函数。 - François Andrieux
1
您的帖子表明您正在迭代指向容器的指针容器,并且您想以一种方便的方式跳过nullptrs。现在,问题是:默认值(nullptr的情况)是否会用于除了干净的解引用之外的任何其他方式?如果没有,也许使用boost::filter_iterator是正确的选择?确实,您会失去范围for循环,但仍然可能值得一试。 - alagner
显示剩余3条评论
6个回答

6

实际上,没有一个好的、符合惯用语的C++解决方案能够完全匹配您所要求的。

"如果值为空则返回空"这种语言可能适用于具有垃圾回收或引用计数对象的语言。因此,在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。


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

4
非常遗憾,没有办法完全实现您想要的效果。您的选项如下:
  • 如果传递的指针是nullptr,则返回对静态对象的引用。只有在返回const引用时才正确,否则将面临巨大的问题;
  • 返回std::optional< std::ref >,如果指针是 nullptr ,则返回未设置的可选项。这并没有真正解决您的问题,因为您仍然必须在调用站点检查可选项是否已设置,而且您可能会在调用站点检查指针是否为 nullptr 。或者,您可以使用value_or提取来自可选数据类型的值,这类似于以不同方式打包的下一个选项;
  • 使用第二个尝试,但删除默认参数。这将强制调用站点提供默认对象-这使代码有些丑陋。

1
optionalvalue_or,使其与选项3“兼容”。 - Jarod42

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

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

2

根据评论:

一种解决方案是实现一个代理范围类型,其中包含一个指针。这种类型将提供begin和end成员,它们可以将调用转发到指向的容器或提供一个空范围。在基于范围的for循环的上下文中使用时,使用方式基本相同于使用NullOrEmpty函数。- François 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 似乎有点棘手。
  • 使用 variant 的解决方案会生成更小的代理对象大小(我认为)。
  • 这肯定比 OP 中的 if+for 运行时更昂贵,毕竟你至少要构造一个(空)临时对象。
    • 如果你只需要 begin() 和 end(),那么提供一个空范围可能会更便宜,但如果这应该推广到除了对 begin() 和 end() 的调用之外的更多情况,你仍然需要一个真正的 T 临时对象。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接