Troskyvs
Troskyvs

Reputation: 8057

When is c++17 std::apply/invoke necessary for programming?

As long as c++ is object-oriented programming language, I'm not sure if std::apply or std::invoke are necessary utility, which converts:

Object.Function(Args...)

into:

std::invoke(Object, Function, Args)

that looks like c-style function call. So my question is, when is this kind of scenario a necessary/convenience for daily programming? Or else, is there a case that using apply/invoke makes things simpler?

Would you help to give a good example case? Thanks a lot!

Upvotes: 2

Views: 810

Answers (2)

topin89
topin89

Reputation: 1

One of the real use of std::apply is tuple unpacking, potentially nested. Here's tested example:


#include <iostream>

#include <string>
#include <tuple>
#include <sstream>

// adapted from here: https://stackoverflow.com/a/48458312
template <typename> 
constexpr bool is_tuple_v = false;

template <typename ...T> 
constexpr bool is_tuple_v<std::tuple<T...>> = true;

template<typename Tval, typename ... T>
void linearize_tuple(std::stringstream &outbuf, const Tval& arg, const T& ... rest) noexcept {
    if constexpr (is_tuple_v<Tval>){
        outbuf << "{ ";
        std::apply([&outbuf](auto const&... packed_values) {
                linearize_tuple(outbuf, packed_values ...);
            }, arg
        );
        outbuf << " }";
    }
    else{
        outbuf << arg;
    }

    if constexpr(sizeof...(rest) > 0){
        outbuf << ' ';
        linearize_tuple(outbuf, rest ...);
    }
}

template<typename ... T>
std::string args_to_string(const T& ... args) noexcept {
    std::stringstream outbuf{};
    if constexpr(sizeof...(args) > 0){
        linearize_tuple(outbuf, args ...);
    }
    return outbuf.str();
}

int main(){
    std::cout << args_to_string(
        "test", 1, "2", 3.0, '0', std::tuple
        {
            "examination", 10, "20", 30.0, '1', std::tuple
            {
                "we need to go deeper", 100, "200", 300, '2'
            }
        }
    );
}

It will print:

test 1 2 3 0 { examination 10 20 30 1 { we need to go deeper 100 200 300 2 } }

and if you look at implementation of std::apply, it's probably using std::invoke like in example from cppreference. Key part:

namespace detail {
template <class F, class Tuple, std::size_t... I>
constexpr decltype(auto) apply_impl(F&& f, Tuple&& t, std::index_sequence<I...>)
{
    // This implementation is valid since C++20 (via P1065R2)
    // In C++17, a constexpr counterpart of std::invoke is actually needed here
    return std::invoke(std::forward<F>(f), std::get<I>(std::forward<Tuple>(t))...);
}

Upvotes: 0

康桓瑋
康桓瑋

Reputation: 42861

std::invoke enables all Callable objects to be invoked uniformly, which contains pointers to member functions and pointers to member variables that cannot be invoked with regular function call form like f(args...)

struct S {
  void f(int i);
  int x;
};

int main() {
  auto mem_vptr = &S::x;
  auto mem_fptr = &S::f;
  S s;
  std::invoke(mem_vptr, s);    // invoke like s.*mem_vptr;
  std::invoke(mem_fptr, s, 0); // invoke like (s.*mem_fptr)(0);
}

Upvotes: 5

Related Questions