【问题标题】:Generic lambda with type traits具有类型特征的通用 lambda
【发布时间】:2018-04-26 16:19:49
【问题描述】:

我有一个通用的 lambda:

auto update = [&](auto& container, shader& effect)
{
    for (const auto& key : objects)
    {
        auto& obj = *container[key];

        if (obj.HasAnyGeometry())
        {
            m_GeometryDrawCalls.push_back({ &obj, effect });
        }
    }
};

它处理我的 3D 对象并将它们添加到绘图调用列表 m_GeometryDrawCalls。所有这些对象都派生自某个自定义类,我们称之为class Object3D。但是我最近添加了不是从Object3D 派生的对象,因此它不需要向m_GeometryDrawCalls 添加几何图形,但它会在内部处理它。我想使用相同的功能来处理这个问题。是否可以通过模板以某种方式实现?基本上我需要为其他类型做的就是:

auto update = [&](auto& container, shader& effect)
{
    for (const auto& key : objects)
    {
        auto& obj = *container[key];
    }
};

有什么想法吗?

【问题讨论】:

  • C++14 还是 C++17?
  • 啊,我忘了说,C++14
  • 可惜……用C++17简单多了
  • 能否请您也发布 C++17 的解决方案?我很好奇,但也希望能够在 C++14 中实现
  • 嗯...要为您的确切问题提供解决方案,您应该向我们展示Object3Dm_GeometryDrawCalls 等。我可以向您展示的最好的方法是两个解决方案(C++14 和C++17) 到一个简化的问题,展示如何通过 SFINAE (C++14) 启用/禁用两个不同的 lambda 或启用部分 lamda (C++17)...给我一些时间

标签: c++ templates lambda c++14


【解决方案1】:

在 C++17 中,您可以这样做:

auto update = [&](auto& container, shader& effect)
{
    for (const auto& key : objects) {
        auto& obj = *container[key];

        if constexpr (std::is_base<Object3D, std::decay_t<decltype(obj)>>::value) {
            if (obj.HasAnyGeometry()) {
                m_GeometryDrawCalls.push_back({ &obj, effect });
            }
        }
    }
};

对于 C++11,您可以使用 struct overloaded 和 SFINAE: 来自c11-overloaded-lambda-with-variadic-template-and-variable-capture

template <class... Fs>
struct overload;

template <class F0, class... Frest>
struct overload<F0, Frest...> : F0, overload<Frest...>
{
    overload(F0 f0, Frest... rest) : F0(f0), overload<Frest...>(rest...) {}

    using F0::operator();
    using overload<Frest...>::operator();
};

template <class F0>
struct overload<F0> : F0
{
    overload(F0 f0) : F0(f0) {}

    using F0::operator();
};

template <class... Fs>
auto make_overload(Fs... fs)
{
    return overload<Fs...>(fs...);
}

然后(我使用 c++14 表示 _t):

auto update = make_overload(
    [&](auto& container, shader& effect)
    -> std::enable_if_t<std::is_base<Object3D,
                                     std::decay_t<decltype(*container.begin())>>::value>
    {
        for (const auto& key : objects) {
            auto& obj = *container[key];

            if (obj.HasAnyGeometry()) {
                m_GeometryDrawCalls.push_back({ &obj, effect });
            }
        }
    },
    [&](auto& container, shader& effect)
    -> std::enable_if_t<!std::is_base<Object3D,
                                      std::decay_t<decltype(*container.begin())>>::value>
    {
        for (const auto& key : objects) {
            auto& obj = *container[key];
        }
    });

【讨论】:

  • 我可能会创建“重载”对象来处理作用于objfor 循环内的工作。这主要是一种风格选择,但确实可以防止重复。
  • 你也可以看看static_if
【解决方案2】:

假设您知道每个对象都会在内部处理其绘图,您可以执行类似...

template<bool b>
using tf_type = std::conditional_t<b, std::true_type, std::false_type>

template<class G, class O, class E>
void add_to_if(std::true_type, G& m_GeometryDrawCalls, const O& obj, const E& effect) {
      m_GeometryDrawCalls.push_back({ &obj, effect });
}

template<class G, class O, class E>
void add_to_if(std::false_type, G& m_GeometryDrawCalls, const O& obj, const E& effect) 
{ /*do nothing*/ }

auto update = [&](auto& container, shader& effect)
{
for (const auto& key : objects)
{
    auto& obj = *container[key];

         //obj.HasAnyGeometry() must return a constexpr bool
        tf_type<obj.HasAnyGeometry()> TF;
        add_to_if(TF, m_GeometryDrawCalls, obj, effect);
    }
}
};

Add_to 将专用于 std::true_type/std::false_type,而 tf_type 根据 constexpr bool 返回适当的类型。

【讨论】:

    【解决方案3】:

    如果我们被困在 C++14 中,那么穷人的 if constexpr 就是编写一个函数,它接受两个函数,然后调用需要的函数:

    template <typename True, typename False, typename... Args>
    decltype(auto) static_if(std::true_type, True&& true_f, False&&, Args&&... args ) {
        return std::forward<True>(true_f)(std::forward<Args>(args)...);
    }
    
    template <typename True, typename False, typename... Args>
    decltype(auto) static_if(std::false_type, True&&, False&& false_f, Args&&... args ) {
        return std::forward<False>(false_f)(std::forward<Args>(args)...);
    }
    

    然后你的身体就是两个通用的 lambda:

    for (const auto& key : objects)
    {
        auto& obj = *container[key];
        static_if(
            // condition
            std::is_base<Object3D, std::decay_t<decltype(obj)>>{},
            // true case. NB we use e throughout, not obj
            [&](auto&& e) {
                m_GeometryDrawCalls.push_back({ &e, effect });
            },
            // false case: noop
            [&](auto&& ) {},
            obj); 
    }
    

    随意根据您的用例重新组织参数。

    【讨论】:

      【解决方案4】:

      我喜欢基于 overloadmake_overload 的 Jarod42 的 C++14 解决方案,但有(Jarod:如果我错了,请纠正我)一个缺点:调用 overload 对象的 operator() 在以下情况下有效一个并且只有一个operator()在继承的类中可用。

      所以你必须按如下方式传递第二个 lambda(通用的)

      [&](auto& container, shader& effect)
      -> std::enable_if_t<!std::is_base<Object3D,
              std::decay_t<decltype(*container.begin())>>::value>
      {
          for (const auto& key : objects) {
              auto& obj = *container[key];
          }
      }
      

      所以只有在禁用第一个 lambda 时才启用它,除了避免与第一个 lambda 发生“冲突”外,没有其他理由禁用它。

      我认为最好允许使用一组特定的参数启用多个 lambda,这称为第一个可用参数。

      所以我建议递归lambda_overload

      template <typename...>
      struct lambda_overload;
      

      使用与 Jarod42 接地 overload 基本相同的接地案例,并使用最后一个 lambda 的 operator()(没有跟随“碰撞”的风险)

      template <typename L>
      struct lambda_overload<L> : public L
       {
         lambda_overload (L l) : L{std::move(l)}
          { };
      
         using L::operator();
       };
      

      但这在递归版本中有点不同。

      它定义了一个模板operator(),它调用func(),将0int)作为第一个参数传递并转发接收到的其他参数

         template <typename ... As>
         auto operator() (As && ... as)
          { return func(0, std::forward<As>(as)...); }
      

      当(且仅当)第一个 lambda 接受 operator() 的给定参数列表时,才启用首选 func()(第一个参数是 int

         template <typename ... As>
         auto func (int, As && ... as)
            -> decltype( std::declval<L0>()(std::forward<As>(as)...) )
          { return L0::operator()(std::forward<As>(as)...); }
      

      并且备份func()(第一个参数是long永远定义并在lambda_overload的以下递归级别中调用operator()

         template <typename ... As>
         auto func (long, As && ... as)
          { return lambda_overload<Ls...>::operator()(std::forward<As>(as)...); }
      

      这样就没有“冲突”的风险,因为如果有多个operator() 可用,它会在第一个可用的时候执行。

      所以make_lambda_overload()可以这样调用

      auto update = make_lambda_overload(
          [&](auto& container, shader& effect)
          -> std::enable_if_t<std::is_base<Object3D,
                std::decay_t<decltype(*container.begin())>>::value>
      {
          for (const auto& key : objects) {
              auto& obj = *container[key];
      
              if (obj.HasAnyGeometry()) {
                  m_GeometryDrawCalls.push_back({ &obj, effect });
              }
          }
      },
      [&](auto& container, shader& effect)
      {
          for (const auto& key : objects) {
              auto& obj = *container[key];
          }
      });
      

      避免第二个通用 lambda 的 SFINAE 禁用部分。

      以下是一个完整(但经过简化)的示例

      #include <iostream>
      
      template <typename...>
      struct lambda_overload;
      
      template <typename L>
      struct lambda_overload<L> : public L
       {
         lambda_overload (L l) : L{std::move(l)}
          { };
      
         using L::operator();
       };
      
      template <typename L0, typename ... Ls>
      struct lambda_overload<L0, Ls...> : public L0, public lambda_overload<Ls...>
       {
         lambda_overload (L0 l0, Ls ... ls)
            : L0{std::move(l0)}, lambda_overload<Ls...>{std::move(ls)...}
          { };
      
         // backup version (ever defined!)
         template <typename ... As>
         auto func (long, As && ... as)
          { return lambda_overload<Ls...>::operator()(std::forward<As>(as)...); }
      
         // preferred version (defined only if operator() defined for L0 type)
         template <typename ... As>
         auto func (int, As && ... as)
            -> decltype( std::declval<L0>()(std::forward<As>(as)...) )
          { return L0::operator()(std::forward<As>(as)...); }
      
         template <typename ... As>
         auto operator() (As && ... as)
          { return func(0, std::forward<As>(as)...); }
       };
      
      template <typename ... Ls>
      auto make_lambda_overload (Ls && ... ls)
       { return lambda_overload<Ls...>{ std::forward<Ls>(ls)... }; }
      
      int main()
       {
         auto l1 = [&](auto const & t) -> decltype((void)t.size())
          { std::cout << "-- with size() version - " << t.size() << std::endl; };
      
         auto l2 = [&](auto const & t)
          { std::cout << "-- generic version (also no size())" << std::endl; };
      
         auto lo = make_lambda_overload(std::move(l1), std::move(l2));
      
         lo(std::string{"0"});  // print "with size() version - 1
         lo(1);                 // print "generic version (also no size()="
       }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-03-30
        • 1970-01-01
        • 1970-01-01
        • 2017-06-24
        • 1970-01-01
        相关资源
        最近更新 更多