Function to use projections in legacy algorithms I love the &quot;projections&quot; introduced by the ranges library, and I would like to use them in algorithms that haven't been rangeyfied yet.
I had the idea of using them through a function like this:
#include &lt;utility&gt;
#include &lt;functional&gt;

template&lt;typename TFn, typename TProj = std::identity&gt;
auto
constexpr proj(TFn &amp;&amp;Fn, TProj &amp;&amp;Proj = {})
{
  return [&amp;Fn, &amp;Proj]&lt;typename... TArgs&gt;(TArgs &amp;&amp;...Args)
  {
    return std::invoke(std::forward&lt;TFn&gt;(Fn),
                       std::invoke(std::forward&lt;TProj&gt;(Proj), std::forward&lt;TArgs&gt;(Args))...);
  };
}

This function enables writting, for example:
  std::sort(std::execution::par_unseq, v.begin(), v.end(), proj(std::less{}, &amp;Pair::Key));

instead of:
  std::sort(std::execution::par_unseq, v.begin(), v.end(), [](auto const &amp;lhs, auto const &amp;rhs) { return lhs.Key &lt; rhs.Key; });
Design review
I don’t think you gave a lot of thought to what this library would be actually useful for.
Let me start by just considering the idea of proj() in complete isolation, ignoring any real-use context, and just considering it as a clever trick. From that perspective, I’d have to say that proj() is not a bad idea… but perhaps not the best way to go about it.
Here’s what a ranges call to sort() might look like:
std::ranges::sort(b, e, {}, &amp;Pair::Key);

Here’s what proj() promises, at best:
std::sort(b, e, proj(std::less{}, &amp;Pair::Key));

Here’s what else is possible in C++17, and possibly even before if you’re willing to put in the work of re-implementing std::invoke, etc. (of course it’s also possible in C++20, but… we’ll get to that issue shortly):
myns::sort(b, e, {}, &amp;Pair::Key);

That is, you could implement a wrapper around std::sort() that handles the projection transparently.
For the user, that’s shorter, simpler, and doesn’t require manually specifying the comparator.
Not to mention that when I can advance to C++20 and use the full power of std::ranges, all I need to do (very basically) is a find-replace for myns to std::ranges. If I had these little proj() sub-calls everywhere, the code transform becomes MUCH more complex, and possibly tedious and/or dangerous.
And while you’re at it, why not implement myns::sort() as a neibloid, to get all the sweet, sweet benefits that go along with that?
So if you really want the benefits of projections, why wouldn’t you do that?
Yes, I realize I’m basically saying “re-implement std::ranges”. As a halfway solution proj() only halfway satisfies. What’s the point of that? If you’re going to implement a library tool to make coders’ lives easier, why half-ass it? Who benefits from a mediocre solution? I guess the person writing the library gets to save on some work… but the whole point of library writing is that the library writer is taking on extra work so library users don’t have to. Making things easier for the library writer is silly. You should be concentrating on making things easier for the users.
And by the way, there are a bunch more complications to making something like proj() generally usable that I haven’t mentioned. Like, you’ve created a variant that works with most of the standard algorithms… but it won’t work with std::find() (no predicate, so no Fn). Won’t work with std::merge() (has a predicate, but has two projections). By the time you sort all that out, and test it all rigorously enough to show that it’s trustworthy in production code, you might as well have just used range-v3.
Except… and this is the biggest issue with this whole idea…
This code won’t even compile in C++17. It needs C++20. C++20… where… std::ranges already exists.
So who exactly is this library for?

It can’t be for C++17 (or before) users. (Won’t compile.)
It makes no sense for C++20 (and later) users. (Because if they want a projection in a legacy algorithm, they can just use the std::ranges version. In your example, std::ranges::sort().)

Are you targeting the hypothetical subset of users who are targeting C++20 with their project, but are using a C++20 compiler/library that has lambda templates, std::identity, and constexpr std::invoke… but not the ranges algorithms? That’s a bizarre target group, to say the least. I mean, who targets a C++ version—or uses a specific feature, like projections—without a compiler/library that actually supports it? Not to mention it’s a rather ephemeral group, because presumably if they’re targeting C++20, they could just use std::ranges as soon as the features they need become available in their chose compiler/library, making proj() just a weird stop-gap they would use for a while, then have to weed out of their code base later.
The bottom line is: proj() makes no sense. The only people who could use it… don’t really need it.
(Or, AT BEST, they could maybe use it NOW, for the brief period when their chosen platform supports everything needed to make proj() work, but doesn’t include std::ranges. But, frankly, I’d rather write the lambda than use a mysterious 3rd-party library that’s only going to be useful for a few months then become a legacy maintenance burden.)
From where I’m sitting, this looks like a case of nerd sniping. A neat little theoretical problem became so distracting by its intellectual challenge, that you completely ignored the surrounding real-world problems that are, in point of fact, much more important.
Code review
There’s not much code to review, of course, but….
auto
constexpr proj(//...

It’s pretty bizarre to write the constexpr after the auto. I’ve never seen that done before. I can’t see any sensible reason to adopt this style; it’s only likely to cause confusion.
constexpr proj(TFn &amp;&amp;Fn, TProj &amp;&amp;Proj = {})

Putting the type modifiers next to the variable name, rather than with the type, is a C convention, not a C++ convention. In C++, types matter more, so it makes more sense to say TFn&amp;&amp; Fn.
Also, it is a little peculiar to name variables with UpperCamelCase. UpperCamelCase is usually reserved for template argument names. In other words, TFn&amp;&amp; fn is more conventional.
template&lt;typename TFn, typename TProj = std::identity&gt;
auto
constexpr proj(TFn &amp;&amp;Fn, TProj &amp;&amp;Proj = {})
{
  return [&amp;Fn, &amp;Proj]&lt;typename... TArgs&gt;(TArgs &amp;&amp;...Args)
  {
    return std::invoke(std::forward&lt;TFn&gt;(Fn),
                       std::invoke(std::forward&lt;TProj&gt;(Proj), std::forward&lt;TArgs&gt;(Args))...);
  };
}

The thing that bothers me most about this function is that you:

take the two function arguments as forwarding references
place them into the closures as lvalue references; then
forward them within the closure.

Now, I will admit here that I haven’t sat down and reasoned this through as hard as it really deserves (because it’s late, and because I don’t see the point of the function in any case), but even from a cursory inspection it smells wrong. The smelly part is that you “forget” you’re dealing with forwarding references on that second step.
If the two parameters are lvalues, then no harm, no foul. All the forwarding references will collapse to lvalue references, and the forward()s will degenerate to… well, nothing, effectively; just lvalue references. So you have lvalue references all the way through. All cool.
But if a parameter is an rvalue, then the forward()s will degrade to rvalue references, and may end up moving the argument. So imagine you have a call to, say, sort(), where proj() has been given an rvalue function object (for either parameter, doesn’t matter). On the first execution of the comparator—that is, the first time that lambda proj() returns is called—it will move the function object… leaving it in a moved-from state… so the second time the comparator/lambda is executed, it’s using an “empty” function object. Exactly what damage this will cause depends on what using a “moved-from” function object means, which will depend on the function object type. But in any case, it’s not good.
Okay, put simply:

You have correctly taken the function arguments as forwarding references…
… and you have correctly perfect-forwarded them in the invoke() expressions…
… but you have NOT perfect-forwarded them in the lambda capture.

You need perfectly-forwarded lambda captures. That is NOT trivial… but not too hard (tip: use std::tuple and a mother-ton of decltype(TFn), etc.; don’t forget to mark the lambda mutable).
Summary
As an experiment to explore stuff like projections and perfect-forwarding, this is actually a really neat idea.
As a practical library… I’m not seeing it. Just about the only people who could use it won’t need it. And the tiny demographic who could use it, but who can’t use the superior std::ranges solution, would be unwise to use it (better to wait until their platform supports std::ranges).
The implementation is sound but for that one tricky hiccup: You have perfectly-forwarded everything except the lambda captures. Fix that, and I think it should be a sound implementation. For full practical usability (such as it is), you’d need a couple of overloads for cases not covered by a single predicate and a single projection (for example, an overload without a predicate, for things like std::find(), std::count(), std::remove(), etc.; plus a few other overloads for the more esoteric cases), but once you’ve got the one working, that should be no problem.