2

I have a generic 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 });
        }
    }
};

which processes my 3D objects and adds them to the draw call list m_GeometryDrawCalls. All of those objects are derived from some custom class, let's call it class Object3D. However I recently added object which is not derived from Object3D so it does not need to add geometry to m_GeometryDrawCalls, but it handles it internally. I would like to use same function to handle this. Is it somehow possible via templates? Basically all I need to do for other type is this:

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

Any ideas?

Drew Dormann
  • 59,987
  • 13
  • 123
  • 180
mezo
  • 423
  • 1
  • 7
  • 19
  • C++14 or C++17? – max66 Apr 26 '18 at 16:22
  • Ah, I forgot to mention, C++14 – mezo Apr 26 '18 at 16:24
  • what a pity... with C++17 is a lot simpler – max66 Apr 26 '18 at 16:25
  • can you also post the solution for C++17 please? I'm curious, but would like to also be able to do it in C++14 – mezo Apr 26 '18 at 16:26
  • Well... to give a solution for your exact problem, you should show us `Object3D`, `m_GeometryDrawCalls`, etc. The best I can show you are two solution (C++14 and C++17) to a simplified problem to show how to enable/disable two different lambda through SFINAE (C++14) or enable part of a lamda (C++17)... give me some minutes – max66 Apr 26 '18 at 16:30
  • c++17 has `if constexpr`, else you can implement overloaded similar to the one provided in [std::visit example](http://en.cppreference.com/w/cpp/utility/variant/visit). – Jarod42 Apr 26 '18 at 16:36
  • @mezo I think you can solve this by overloading your lambda. Don't think that `std::overload` in in the std yet (maybe it was renamed) but it's pretty easy to do yourself. – Dan M. Apr 26 '18 at 16:43
  • Well... as usual, Jarod42 has written a solution better than mine where I'm still fighting with my overload... I suggest you to study the C++14 Jarod's way (and as you can see, the C++17 way (`if constexpr`) is a lot simpler) – max66 Apr 26 '18 at 16:51

4 Answers4

3

In C++17, you may simply do:

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

For C++11, you may use struct overloaded and SFINAE: From 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...);
}

and then (I use c++14 for _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];
        }
    });
Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • I probably would have created the "overload" object to handle just the work inside the `for` loop acting on `obj`. It's mostly a style choice, but it does prevent a little repetition. – aschepler Apr 26 '18 at 21:47
  • 1
    You might also look at [static_if](https://baptiste-wicht.com/posts/2015/07/simulate-static_if-with-c11c14.html). – Jarod42 Apr 26 '18 at 21:56
0

Assuming that you know that each object would handle its drawing internally or not you could do something like...

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 will be specialized for std::true_type/std::false_type while tf_type returns the apropriate type based upon a constexpr bool.

0

If we're stuck in C++14, a poor man's if constexpr is to write a function that takes two functions and just calls whichever one is desired:

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

And then your body is just two generic lambdas:

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

Feel free to reorganize arguments as suits your use-case.

Barry
  • 286,269
  • 29
  • 621
  • 977
0

I like the Jarod42's C++14 solution based over overload and make_overload but has (Jarod: correct me if I'm wrong) a drawback: calling a operator() of a overload object works if one and only one operator() is available in inherited classes.

So you have to pass the second lambda (the generic one) as follows

[&](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];
    }
}

so enabling it only when is disabled the first lambda, when there is no other reason to disable it except to avoid a "collision" with the first lambda.

I think should be preferable permit that more than one lambda is enabled with a particular set of argument and that is called the first available.

So I propose a recursive lambda_overload

template <typename...>
struct lambda_overload;

with a ground case that is substantially identically to the Jarod42 ground overload and use the operator() of the last lambda (no risk of following "collisions")

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

   using L::operator();
 };

but that is a little different in the recursive version.

It define a template operator() that call a func(), passing 0 (a int) as first argument and forwarding the other arguments received

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

A preferred func() (the first argument is an int) is SFINAE enabled if (and only if) the first lambda accept the given argument list for an operator()

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

and a backup func() (the first argument is a long) is ever defined and call the operator() in the following recursion level for lambda_overload

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

This way there isn't risk of "collision" because if more than operator()'s is available, it's executed the first one available.

So make_lambda_overload() could be called as follows

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

avoiding the SFINAE disable part for the second generic lambda.

The following is a full (but simplified) example

#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()="
 }
max66
  • 65,235
  • 10
  • 71
  • 111