Is std::tag_invoke just an object with operator() that perfectly forwards all args to unqualified tag_invoke call?
Asked Answered
T

1

8

I understand that std::tag_invoke is not technically necessary, but what does it do?

I thought the point of tag_invoke was that you could customize some::cpo for your type Foo by overloading tag_invoke(decltype(some::cpo), Foo const&); template<typename T> auto Cpo::operator(T&& arg) would be implemented as an unqualified call to tag_invoke(some::cpo, arg), so the call would find your overload of tag_invoke via ADL, resulting in routing cpo(Foo{}) to tag_invoke(decltype(some::cpo), Foo const&).

But then how is this possibile via an std::tag_invoke object? I don't see it defined in P1895R0.

Is it truly just something like this?

namespace std {
inline constexpr struct TagInvoke {
    inline constexpr decltype(auto) operator()(auto&& ...args) const {
        return tag_invoke(std::forward<decltype(args)>(args)...);
    }
} tag_invoke;

Furthemore, the proposal calls it a meta-customization-point-object. To me, if it's defined as in the previous snippet, it's not a CPO at all. And you don't even want it to be, because it must always do the same thing, which is forward everything to an unqualified tag_invoke call.

Am I missing something?

Tocology answered 23/6 at 12:57 Comment(7)
The proposal leaves the definition of std::tag_invoke to implementation. For immunity against accidental overload in std namespace, it has to be a CPO. It can be implemented as a function pointer/reference or a closure or a monostate. Using the std::tag_invoke inside a CPO definition is not essential, but constraints the implementations to use certain signature for their dependent tag_invoke definitions. Leting a CPO directly use ADL would invite future bugs into the code, by laxing the signature of tag_invoke.Stealage
@Red.Wave, but if the way the implementers customize the cpo is by defining tag_invoke, then ADL must happen somewhere to pick the appropriate definition, no?Tocology
The point is that CPO can not be overloaded at original namespace scope. So we need a way to delegate its implementation to consumer libraries, but at the same time we need assure that the signature is fixed at the provider library. std::tag_invoke delegates the task to ADL tag_invoke(std::tag_t, args...); thus the call signature is dictated by the CPO definition, while implementation is left to user code. Namespaces are assumed to be the boundaries between libraries; Every well-defined library must break into multiple nested namespaces.Stealage
The CPO definition calls std::tag_invoke, then the implementation of std::tag_invoke is supposed to delegate to(invoke) ADL, but fix the very first argument to std::tag_t<CPO>; so the ADL implemented tag_invoke must use the correct tag as first argument.Stealage
@Red.Wave, please, show me some code in an answer.Tocology
Current answer is good. It's just a bit disorganized. mylib::foo is the CPO definition, so it defines its invocation as a call to std::tag_invoke(std::tag_t<foo>{}, args...). And bar wants foo(bar{}) to work; so it defines friend int tag_invoke(std::tag_t<mylib::foo>, bar); as ADL function; the first argument has to be std::tag_t<mylib::foo>. std::tag_t is essentially an identity type, but it creates the minimum required type-safety and code readability.Stealage
@Red.Wave, then improve that answer, if you think just a bit disorganized. IMHO, neither that answer nor your comments are telling me what a plausible definition of std::tag_invoke could be.Tocology
T
8

It is used to implement customization points.

Lets start with the "traditional" way to create a cpo:

constexpr struct my_cpo_type {
  decltype(auto) operator()(auto&&...args) const {
    return my_cpo(std::forward<decltype(args)>(args)...);
  }
} my_cpo;

Now, I can do a my_cpo(std::vector<int>{1,2,3}), and ADL lookup in the body of my_cpo_type::operator() can find an implementation of my_cpo specific to the arguments passed. This can be found within the namespace of my_cpo_type or (if it wasn't illegal) in the namespace of std::vector or (if it existed) in the namespace of int.

The object my_cpo and overload set of the function my_cpo both have the same name, so that can be a bit confusing in how it works, but the use is pretty clear; self documenting even.

The problem is that the names of the cpo function overload set are effectively reserved in every namespace, because if you have a function with that name it could accidentally be picked up by the cpo object and generate nonsense.

To address this problem, a meta cpo is created. The tag-dispatch based tag_invoke meta-cpo acts like a cpo, but its purpose is to allow you to implement other cpos without infecting every namespace with a demand for your specific function name has special meaning.

How does this work?

You start with a customization point object:

namespace mylib{
  constexpr struct MyCpo {
    auto operator()(auto&&...)const;
  } foo;
}

(The operator() is incomplete).

Users just do:

mylib::foo(some arguments)

and forget about it. Implementors do:

namespace other {
  struct bar {
    friend int tag_invoke(std::tag_t<mylib::foo>, bar){
      return 7;
    }
  };
}

to say "here is my implementation of mylib::foo(bar{});. This is the customization point.

tag_invoke object and name is merely used as a tool to allow this customization without poluting every namespace with piles of new special names like foo: instead, everyone routes through one special name tag_invoke.

The implementation of mylib::foo::operator() is:

template<class...Ts>
decltype(auto) operator()(Ts&&...ts)const{
  return std::tag_invoke(std::tag_t<foo>{}, std::forward<Ts>(ts)...);
}

(with maybe some upgrades, like an explicit -> return type). This then invokes tag_invoke, which finds the friend of bar with a matching tag_t overload, and calls it.

So

Bar b;
std::cout << mylib::foo(b);

prints 7.

mylib::foo is a customization point object (cpo). It is impemented using the meta-cpo tag_invoke. The customization of the behavior of mylib::foo is implemented by overloading tag_invoke( tag_t<mylib::foo>, Bar ) in the namespace of Bar - in particular, I used the friend technique to make it only considered when an argument is actually Bar (or is a template with Bar as a type), which reduces error message spam and probably speeds up lookup.

Every "traditional" cpo has both an object named X and an set of overloaded functions called X. The object X dispatches to the overload set of X to find the actual behavior. tag_invoke replaces this with a single cpo that can handle customization points for any number of cpos; this is because we the overload set name isn't namespace restricted, and we don't want to pollute everyone's namespace with a demand that our specific token have a specific meaning.

Transferor answered 23/6 at 14:28 Comment(7)
Sorry, but I don't understand how this answers my question. You write tag_invoke several times, std::tag_invoke once, and you don't tell what it is or what it does.Tocology
@enlico I demonstrated it doing something: I used it to implement the cpo foo. Maybe you don't know what cpos are? Or how they work, absent tag_invoke? You asked 3 questions: I answered the last one, addressing the 2nd to last paragraph, and treated the remainder as rhetorical.Transferor
Maybe you don't know what cpos are? The cpo, as you also write is the thing whose behavior can be customized, foo in your example. The customization point is where the customization happens, which is where the overload of tag_invoke is declared for arguments the cpo itself (or its tag, whatever) and the argument for which the customization is being done. But that's roughly what I wrote in the second paragraph of my question, no? What I'm asking is what's the object std::tag_invoke, what does it do? I've even attempted writing a snippet of what I think it is, ...Tocology
... but you haven't commented on it, even though you wrote both called tag_invoke( and std::tag_invoke( without telling how the latter relates to the former. Plus, it's unclear to me what you mean by 3 questions. I'd say I've asked fundamentally 1 question, but even if I wanted to count the ?, they would be 5, title included, so what 3 are you referring to, specifically? And what's the one you've addressed?Tocology
By the way, when I wrote tag_invoke(decltype(some::cpo), I mean fundamentally the same thing you wrote as tag_invoke(std::tag_t<mylib::foo>, even if I might have been a bit imprecise (const, &, whatever).Tocology
@Tocology I said you asked 3 questions because you used "?" 3 times, each one indicates a question. Actually 4. "But then how is this possibile via an std::tag_invoke object?", "but what does it do?" "to unqualified tag_invoke call?" "Am I missing something?" - I have added more details on how tag_invoke works, and why it is useful, and why it is called a meta-customization-point-object. I can't answer some of these (like "are you missing something") because I cannot understand everything you know or don't know, so I can just explain what is going on. If it aligns with your understanding?Transferor
What is std::tag_invoke? Is it a function? Is it an object with operator()? Please, show me how it is defined.Tocology

© 2022 - 2024 — McMap. All rights reserved.