The std::invoke function does more than invoke functions

Raymond Chen

The std::invoke function in the C++ standard library is usually used to call a functor with parameters.

std::function<void(int)> func = ...;

// same as func(42)
std::invoke(func, 42);

What std::invoke brings to the table is that you can use it for other things beyond just functors.

struct S
{
    void do_something(int);
    int v;
};

S s;
// same as s.do_something(42)
std::invoke(&S::do_something, s, 42);
std::invoke(&S::do_something, std::ref(s), 42);

S* p = &s;
// same as p->do_something(42)
std::invoke(&S::do_something, p, 42);

But wait, what about this?

struct S
{
    std::function<void()> do_something;
    int v;
};

S s;
s.do_something = []() { std::cout << "hello"; };

// does not print anything
std::invoke(&S::do_something, s);

What’s going on here?

One thing that often goes overlooked is that you can also use std::invoke with pointers to non-static data members.

S s;

// same as s.v = 42
std::invoke(&S::v, s) = 42;

// same as "auto x = s.v;"
auto x = std::invoke(&S::v, s);

Invoking a pointer to a non-static data member is the same as dereferencing the pointer, when applied to the second argument.

The statement

std::invoke(&S::do_something, s);

is therefore equivalent to

s.do_something;

which, despite its name, does nothing: It accesses the member and throws it away.

If you want to access the memory and then invoke it, you’ll have to follow up the std::invoke with the function call.

std::invoke(&S::do_something, s)();

Or, if you really like to show off,

std::invoke(std::invoke(&S::do_something, s));

Taken to an extreme, you get invoke-oriented programming!

// Old and busted
this->dict.find(3)->second = "meow";

// New hotness
std::invoke(
    static_cast<std::map<int, std::string>::iterator
        (std::map<int, std::string>::*)(int const&)>(
        &std::map<int, std::string>::find),
        std::invoke(&MyClass::dict, this), 3)->second = "meow";

// Beyond hot
std::invoke(
    static_cast<std::string& (std::string::*)(char const*)>
        (&std::string::operator=), 
    std::invoke(&std::pair<int const, std::string>::second,
        std::invoke(
            static_cast<std::pair<int const, std::string>& (
                std::map<int, std::string>::iterator::*)() const noexcept>
                (&std::map<int, std::string>::iterator::operator*),
        std::invoke(
            static_cast<std::map<int, std::string>::iterator
                (std::map<int, std::string>::*)(int const&)>
                (&std::map<int, std::string>::find),
            std::invoke(&MyClass::dict, this), 3))), "meow");

The above code is technically non-portable thanks to [member.functions], which says

For a non-virtual member function described in the C++ standard library, an implementation may declare a different set of member function signatures, provided that any call to the member function that would select an overload from the set of declarations described in this document behaves as if that overload were selected.

This means basically that you cannot form pointers to non-virtual member functions, because the implementation’s signature for the member function is permitted to differ from the formal definition (say, by the addition of default template arguments or parameters), as long as the behavior is the same. In practice, these extra default arguments or parameters are used for things like SFINAE.

To make the code portable, we’ll have to wrap the member function pointers into program-provided versions.

namespace mfptr
{
    template<typename Object, typename...Args>
    decltype(auto) find(Object&& object, Args&&...args) {
        return std::forward<Object>(object).find(std::forward<Args>(args)...);
    }

    template<typename Object>
    decltype(auto) dereference(Object&& object) {
        return *std::forward<Object>(object);
    }

    template<typename Object, typename Arg>
    decltype(auto) assign(Object&& object, Arg&& arg) {
        return std::forward<Object>(object) = arg;
    }
}

std::invoke(
    &mfptr::assign<std::string&, char const*>, 
    std::invoke(&std::pair<int const, std::string>::second,
        std::invoke(
            &mfptr::dereference<std::map<int, std::string>::iterator>, 
            std::invoke(
                &mfptr::find<std::map<int, std::string>&, int>,
                std::invoke(&MyClass::dict, this), 3))), "meow");

3 comments

Discussion is closed. Login to edit/delete existing comments.

  • 紅樓鍮 0

    I don’t quite see the point of std::invoke. The only 2 cases where it extends the usual function call syntax are non-static member functions and variables; it may be useful for std::invoke to support member functions (using a lambda to wrap the member function would require variadic templates and perfect forwarding, which is verbose), but supporting member variables is just confusing; I just can’t conceive anyone who would find this feature useful.

    And even then, std::mem_fn completely covers the member function use case, in a more readable way (being obviously analogous to a wrapper lambda) and requiring no additional concepts.

    • Mike Winterberg 0

      Raymond’s teasing a bit because of April Fool’s, but invoke is meant to lessen the need for callers to use things like mem_fn. For instance, the ranges algorithms use invoke extensively, so using simple member based selections for projections need less syntax noise than they do with the classic algorithms.

      https://gcc.godbolt.org/z/WeY594Ma5

      Also note that even the “classic” callable wrappers like mem_fn, bind, and function support pointers to member data as a callable.
      https://gcc.godbolt.org/z/ndh4Eo9Mv

      Which makes sense since member data access is just syntactic sugar for a function.

  • Dwayne Robinson 0

    C++23’s deducing this should greatly simplify these cases for me (obviating std::invoke for my uses anyway) as I can now consistently call either a static function taking a class/struct or a semistatic method on a class/struct via the same function pointer. Been playing with it already in VS2022 Preview 2.

Feedback usabilla icon