April 1st, 2022

The std::invoke function does more than invoke functions

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");
Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

3 comments

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

  • Dwayne Robinson

    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.

  • 紅樓鍮

    I don't quite see the point of . The only 2 cases where it extends the usual function call syntax are non-static member functions and variables; it may be useful for 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,...

    Read more
    • Mike Winterberg

      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...

      Read more