3

Here I present a first cut of two variants of the template function over(vec, f).

Both versions iterate over a vector-like object and call a function object for each element.

One version calls the function object with two arguments - an element reference and an index - the second with just the element reference.

The idea is to get the compiler to select the version that matches the passed-in lambda, so the user can express intent in the lambda signature without having to select a differently-named free function.

here's the code:

#include <vector>
#include <iostream>

template<typename... Ts> struct make_void { typedef void type;};
template<typename... Ts> using void_t = typename make_void<Ts...>::type;


template<class Vector, class F>
auto over(Vector &&vec, F &&f)
-> void_t<decltype(f(vec.operator[](std::declval<std::size_t>()), std::declval<std::size_t>()))>
{
    const auto size = vec.size();
    for (std::size_t i = 0; i < size; ++i) {
        f(vec[i], i);
    }
}


template<class Vector, class F>
auto over(Vector &&vec, F &&f)
-> void_t<decltype(f(*vec.begin()))>
{
    for (auto &&x : vec) {
        f(x);
    }
}

int main() {
    std::vector<float> vf = {1.0, 1.1, 1.2};

    std::cout << "two-argument form:\n";
    over(vf, [](auto &&val, auto &&index) {
        std::cout << index << " : " << val << std::endl;
    });

    std::cout << "\none-argument form:\n";
    over(vf, [](auto &&val) {
        std::cout << val << std::endl;
    });
}

Question:

You will see that the clause inside the void_t<> return type generator knows all about the implementation of the function. I am displeased by this as:

a) it's leaking implementation details in the interface, and

b) it's not DRY.

Is there a better way to achieve this which:

a) allows the implementation to change without changing the template-enabler,

b) doesn't look like my dogs had a play-fight on my keyboard?

Richard Hodges
  • 68,278
  • 7
  • 90
  • 142

3 Answers3

2

For this example, avoiding the "repetition" is going to be way more work/complexity than the repetition itself, but the basic idea is to count the ar-iness of the function, and then dispatch appropriately. A very similar problem is discussed here: Call function with part of variadic arguments. Using the implementation of function_traits you can can implement a function called dispatch (I called it foo in my answer to that question):

template<typename F, std::size_t... Is, class Tup>
void dispatch_impl(F && f, std::index_sequence<Is...>, Tup && tup) {
    std::forward<F>(f)( std::get<Is>(std::move(tup))... );
}

template<typename F, typename... Args>
void dispatch(F && f, Args&&... args) {
    dispatch_impl(std::forward<F>(f),
             std::make_index_sequence<function_traits<F>::arity>{},
             std::forward_as_tuple(args...) );
}


template<class Vector, class F>
void over(Vector &&vec, F &&f)
{
    std::size_t i = 0;
    for (auto &&x : vec) {
        dispatch(std::forward<F>(f), x, i);
        ++i;
    }
}

This answer is 14 compliant as well. Live example: http://coliru.stacked-crooked.com/a/14750cef6b735d7e.

Edit: This approach does not work with generic lambdas. So another approach would be to implement dispatch this way:

template<typename F, typename T>
auto dispatch(F && f, T && t, std::size_t i) -> decltype((std::forward<F>(f)(std::forward<T>(t)),0)) {
    std::forward<F>(f)(std::forward<T>(t));
    return 0;
}

template<typename F, typename T>
auto dispatch(F && f, T && t, std::size_t i) -> decltype((std::forward<F>(f)(std::forward<T>(t), i),0)) {
    std::forward<F>(f)(std::forward<T>(t),i);
    return 0;
}
Nir Friedman
  • 17,108
  • 2
  • 44
  • 72
  • hmm. This is both good and bad. On the one hand it's DRY but on the other it requires a common implementation. I'll sit with it for a bit. – Richard Hodges Sep 07 '17 at 18:46
  • @RichardHodges I guess I just don't follow how you can get both DRY and avoid requiring a common implementation? Maybe it would help if you posted a more complex example (your 20 liner), so we can see which parts are really common and which are not. Also note: this approach does not work with generic lambdas. I prefer to specify the types of arguments to my lambdas so this does not bother me, but I can see above that you used `auto`. – Nir Friedman Sep 07 '17 at 18:52
  • I mean DRY in the sense that we don't have to repeat the inner workings of the algorithm in the enable_if chicanery (which you have achieved here), but at the expense of losing the ability to switch algorithms depending on the provided lambda (which is in my motivating example). – Richard Hodges Sep 08 '17 at 02:55
  • I have posted an answer in which each version of the op formally indicates the argument requirements of F, and then selects on compatibility. On the one hand it's kind of nice because it formally states the contract, on the other it's a little wordy. I'd value your view. – Richard Hodges Sep 08 '17 at 04:30
1

In C++17, you might use SFINAE based on std::is_invocable, something similar to:

template <class Vector, class F>
std::enable_if_t<std::is_invocable<F,
                                   typename Vector::value_type,
                                   std::size_t>::value>
over(const Vector& vec, F&& f)
{
    const auto size = vec.size();
    for (std::size_t i = 0; i < size; ++i) {
        f(vec[i], i);
    }
}

template <class Vector, class F>
std::enable_if_t<std::is_invocable<F, typename Vector::value_type>::value>
over(const Vector& vec, F&& f)
{
    const auto size = vec.size();
    for (const auto& e : vec) {
        f(e);
    }
}
Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • 1
    what if the vector is const or volatile? – Richard Hodges Sep 07 '17 at 16:51
  • Then indeed, we need a trait to obtain `decltype(vec[0])` without repeating that. (but anyway, writing with traits or with `decltype` is mostly repetition with implementation :( ). – Jarod42 Sep 07 '17 at 17:14
0

OK, here's my first serious attempt.

Is there something better than this?

#include <vector>
#include <iostream>
#include <string>

namespace notstd
{
    /* deduce the traits of a container argument, even if it's an rvalue-reference */
    template<class T>
    struct container_traits
    {
        static_assert(not std::is_pointer<T>(), "");
        using without_reference_type = std::remove_reference_t<T>;
        using base_type = std::remove_cv_t<without_reference_type>;

        static constexpr auto is_const = std::is_const<without_reference_type>::value;
        static constexpr auto is_volaile = std::is_volatile<without_reference_type>::value;

        using base_value_type = typename base_type::value_type;
        using value_type = std::conditional_t<is_const, std::add_const_t<base_value_type>, base_value_type>;
    };

    template<class Function, class...Args>
    struct is_compatible_function
    {
        template<class FArg> static auto test(FArg&& f) -> decltype(f(std::declval<Args>()...), void(), std::true_type());
        static auto test(...) -> decltype(std::false_type());

        static constexpr auto value = decltype(test(std::declval<Function>()))::value;
    };
}

/**
 * define the 2-argument algorithm, plus provide function compatibility checks
 */
template<class Vector, class Function>
struct over_op_2
{
    using arg_1_type = std::add_lvalue_reference_t<typename notstd::container_traits<Vector>::value_type>;
    using arg_2_type = std::size_t;

    static constexpr auto is_compatible_function = notstd::is_compatible_function<Function, arg_1_type, arg_2_type>::value;

    template<class VectorArg, class FunctionArg> 
    void operator()(VectorArg&& vec, FunctionArg&& f) const
    {
        std::size_t i = 0;
        for (auto &&x : vec) {
            f(x, i);
            ++i;
        }
    }
};

/**
 * define the 1-argument algorithm, plus provide function compatibility checks
 */
template<class Vector, class Function>
struct over_op_1
{
    using arg_1_type = std::add_lvalue_reference_t<typename notstd::container_traits<Vector>::value_type>;

    static constexpr auto is_compatible_function = notstd::is_compatible_function<Function, arg_1_type>::value;

    template<class VectorArg, class FunctionArg> 
    void operator()(VectorArg&& vec, FunctionArg&& f) const
    {
        for (auto &&x : vec) {
            f(x);
        }
    }
};

/**
 * Choose op_2 if the Function type will allow it, otherwise op_1 if that's possible, otherwise void (error)
 */
template<class Vector, class Function>
struct select_over_op
{
    using op_1 = over_op_1<Vector, Function>;
    using op_2 = over_op_2<Vector, Function>;
    using type = std::conditional_t
    <
        op_2::is_compatible_function, 
        op_2, 
        std::conditional_t
        <
            op_1::is_compatible_function, 
            op_1,
            void
        >
    >;

    static_assert(not std::is_same<type, void>(), "function signatures are incompatible");
                                                                                                                ;
};

/**
 * iterate over a vector-like container, calling f(elem, i) if available or f(elem) if not.
 * @param vec is a reference to a vector-like object
 * @param f is a function which is compatible with one of:
 *        void([const]value_type&, std::size_t), or
 *        void([const]value_type&)
 */
template<class Vector, class F>
decltype(auto) over(Vector &&vec, F &&f)
{
    auto op = typename select_over_op<decltype(vec), decltype(f)>::type();
    return op(std::forward<Vector>(vec), std::forward<F>(f));    
}



int main() {
    std::vector<double> v{4.1,5.1,6.1};
    over(v, [] (auto x, auto y) { std::cout << x << ", " << y << "\n"; });
    over(v, [] (auto && x, auto&& y) { std::cout << x << ", " << y << "\n"; });
    over(v, [] (auto const& x, auto const& y) { std::cout << x << ", " << y << "\n"; });
    over(v, [] (auto x, auto&& y) { std::cout << x << ", " << y << "\n"; });
    over(v, [] (auto && x, auto&& y) { std::cout << x << ", " << y << "\n"; });
    over(v, [] (auto const& x, auto&& y) { std::cout << x << ", " << y << "\n"; });
    over(v, [] (auto x) { std::cout << x << "\n"; });
    over(v, [] (auto const& x) { std::cout << x << "\n"; });
    over(v, [] (auto && x) { std::cout << x << "\n"; });

    // converting to int ok (but meh)
    over(v, [] (int x) { std::cerr << x << "\n"; });

    // converting to string correctly fails
    // over(v, [] (std::string x) { std::cerr << x << "\n"; });

    // const vector...
    const std::vector<double> vc{4.1,5.1,6.1};
    over(vc, [] (auto && x, auto&& y) { std::cout << x << ", " << y << "\n"; });

    // breaking const contract on the value_type also fails
    // over(vc, [] (double& x, auto&& y) { std::cout << x << ", " << y << "\n"; });

    return 0;
}

http://coliru.stacked-crooked.com/a/cab94488736b75ed

Richard Hodges
  • 68,278
  • 7
  • 90
  • 142