【问题标题】:Is it possible to implement a DefaultIfNull function in C++?是否可以在 C++ 中实现 DefaultIfNull 函数?
【发布时间】:2021-05-18 13:35:02
【问题描述】:

免责声明:这与其说是缺乏其他解决方案,不如说是出于好奇!

是否可以在 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;
    }
}

这消除了临时的警告和somewhat extends the lifetime,但我认为它仍然很容易出错。


背景:

整个事情是由如下所示的访问模式触发的:

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&lt;std::reference_wrapper&lt;T&gt;&gt;。如果你真的想返回一个引用,你需要某种全局或静态实例来引用。您不能在函数内当场创建实例并返回对它的引用。只有当你返回一个 const 引用时,这才真正有效。您不想将非常量引用传递给标记值,因为任何人都可以更改它。
  • 我不做c++,但new不分配必须删除的内存?这似乎是一种方式
  • 使用const,你可能有一个static const T dummy; return dummy;(没有const,返回的可变实例将被共享:-/所以值将是不可预测的)。
  • 一种解决方案是实现一个包含指针的代理范围类型。这种类型将提供beginend 成员,它们要么将调用转发到指向的容器,要么提供一个空范围。在基于范围的 for 循环的上下文中,用法与使用 NullOrEmpty 函数基本相同。
  • 您的帖子表明您正在迭代一个包含指向容器的指针的容器,并且您想以一种方便的方式跳过nullptrs。现在,问题是:除了干净的取消引用之外,是否会以任何其他方式使用默认值(nullptr 的情况)?如果没有,也许使用boost::filter_iterator 是要走的路?诚然,你失去了远程 for 循环,但它仍然是值得的。

标签: c++ object-lifetime range-based-loop


【解决方案1】:

是的,但是会很丑:

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

【讨论】:

    【解决方案2】:

    没有真正符合您要求的良好、惯用的 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 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。

    【讨论】:

      【解决方案3】:

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

      • 如果传递的指针为 nullptr,则返回对静态对象的引用。仅当您返回 const 引用时,这才是正确的,否则,您会将自己暴露在一大堆蠕虫中;
      • 如果指针为nullptr,则返回std::optional&lt;std::ref&gt; 并返回未设置的可选值。这并不能真正解决您的问题,因为如果设置了optional,您仍然必须在呼叫站点检查,您不妨在呼叫站点检查指针是否为nullptr。或者,您可以使用value_or 从可选中提取值,这类似于不同包装中的下一个选项;
      • 使用第二次尝试,但删除默认参数。这将要求调用站点提供默认对象 - 这会使代码有些难看

      【讨论】:

      • optional 具有 value_or 使其与选项 3“兼容”。
      【解决方案4】:

      如果您只想轻松跳过nullptrs,则可以使用boost::filter_iterator。 现在,这不会在出现空指针时返回默认值,但 OP 的原始代码也不会;相反,它包装容器并提供 API 以在 for 循环中静默跳过它。

      为了简洁,我跳过了所有样板代码,希望下面的 sn-p 很好地说明了这个想法。

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

      【讨论】:

        【解决方案5】:

        如果您接受std::shared_ptr&lt;T&gt;,您可以使用它们以一种相当节省和便携的方式实现这一目标:

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

        【讨论】:

          【解决方案6】:

          来自cmets:

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

          这与另一个答案中的Ayxan provided 基本相似,尽管这里的这个通过提供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 的真正临时对象。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2010-09-20
            • 2013-02-07
            • 2016-01-16
            • 2011-07-09
            • 2020-01-07
            • 2012-12-29
            • 1970-01-01
            相关资源
            最近更新 更多