isocpp / CppCoreGuidelines

The C++ Core Guidelines are a set of tried-and-true guidelines, rules, and best practices about coding in C++
http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines
Other
42.94k stars 5.44k forks source link

When to use trailing return type syntax #2066

Open ntrel opened 1 year ago

ntrel commented 1 year ago

Inspired by: https://blog.petrzemek.net/2017/01/17/pros-and-cons-of-alternative-function-syntax-in-cpp/

More generally, it would be good to recommend trailing return whenever the return type has more than one alpha-numeric token, so the function name is easy to read quickly. (This would obviate the need for the first two rules above).

Eisenwave commented 1 year ago

I think the rules for this would be pretty complicated. For example, if you use std::enable_if_t in the return type, or some other traits or forms of doing SFINAE, trailing return types make sense due to the sheer length.

Trailing return types in general are a weird hack in the language, and

auto main() -> int

is something that almost no one teaches, and that is also really counter-intuitive at first glance (why is it auto?!).

I think trailing return types have been, and probably will always be more of a desperate resort, rather than a default.

claimred commented 1 year ago

But functions names alignment is so nice. 🤌

https://github.com/jship/CpperoMQ/blob/7a685b078ab172d8ad8c3ce6910c2f2e9f7e0d1e/include/CpperoMQ/Mixins/SendingSocket.hpp#L46C1-L46C1

Perhaps not the best example, but imagine something like this. Also Rust, Carbon etc. go with putting return type on the right, they don't have auto though.


auto foo1() -> int;
auto foo2() -> double;
auto foo3() -> std::shared_ptr<TcpConnection>;
auto foo4() -> Point2D;
auto foo5() -> std::unique_ptr<Point2D>;

int foo1();
double foo2();
std::shared_ptr<TcpConnection> foo3();
Point2D foo4();
std::unique_ptr<Point2D> foo5();
jwakely commented 1 year ago

But functions names alignment is so nice. :pinched_fingers:

You don't need trailing return types for that.

int
foo1();

double
foo2();

std::shared_ptr<TcpConnection>
foo3();

Point2D
foo4();

std::unique_ptr<Point2D>
foo5();
BenjamenMeyer commented 1 year ago

Trailing return types are used in languages like Python and JavaScript as Type Hints what will be returned since they do not have type enforcement. For C++ it makes no sense, and if the caller is using auto as the return type and trying to use type hinting to suggest what auto will be then I would say that the API is wrong and that auto should be removed in favor of the actual type being returned.

jwakely commented 1 year ago

if the caller is using auto as the return type and trying to use type hinting to suggest what auto will be

Why would anybody be trying to use it to mean something it doesn't mean? A trailing return type isn't a hint, it is the return type.

BenBE commented 1 year ago

I think trailing return types are sometimes unavoidable when the order of places a template argument is referenced decides on whether it can easily inferred into the type to use …

template<typename T>
auto foo(T a, T b) -> std::vector<T>;

That said, IMHO the use should be kept to a minimum and best be reserved for situations where there's no other (easy) option:

template<typename T>
auto bar(T x) -> decltype([=](){ return x + x; });
BenjamenMeyer commented 1 year ago

if the caller is using auto as the return type and trying to use type hinting to suggest what auto will be

Why would anybody be trying to use it to mean something it doesn't mean? A trailing return type isn't a hint, it is the return type.

@jwakely 100% agree. I'm pointing to what people coming from other languages where type hinting is used would see it; but it's also 100% why C++ doesn't need it. Just use the type and skip using auto.

BenjamenMeyer commented 1 year ago

@BenBE auto is completely unnecessary in those case and just complicates it

template<typename T>
auto foo(T a, T b) -> std::vector<T>;

Or just do:

template<typename T>
std::vector<T> foo(T a, T b);
template<typename T>
auto bar(T x) -> decltype([=](){ return x + x; });

Or rather:

template<typename T>
T bar(T x) -> decltype([=](){ return x + x; });

EDIT: Yeah - messed up the second one; see https://github.com/isocpp/CppCoreGuidelines/issues/2066#issuecomment-1652287140 for the proper version

jwakely commented 1 year ago

The latter isn't valid C++

jwakely commented 1 year ago
template<typename T>
decltype(std::declval<T&>() + std::declval<T&>()) fun(T x);

vs

template<typename T>
auto fun(T x) -> decltype(x + x);

Is auto necessary here? No, but it's arguably nicer.

Edit: changed function name to avoid confusion with the bar above.

BenjamenMeyer commented 1 year ago

@jwakely thanks for the fix of my 2nd one; though I disagree that using auto is nicer.

BenBE commented 1 year ago

Which is not quite the same as in my example of bar which returns a lambda instead of the type that T.operator+(T) would return … Nonetheless, in practical code I'd rather advise people to return std::function<T()> instead …

jwakely commented 1 year ago

Right, I wasn't trying to show the same as your lambda, just another (simpler) case where the trailing return type allows reuse of parameter names. I don't think your lambda example is valid C++ either, is it? The lambda in the return type would not be the same as the lambda in the function body, so you can't do that.

FrankHB commented 2 months ago

if the caller is using auto as the return type and trying to use type hinting to suggest what auto will be

Why would anybody be trying to use it to mean something it doesn't mean? A trailing return type isn't a hint, it is the return type.

It actually isn't, before it is known well-formed. Given the fact that C++ is a language with horrible states not only in the parser for the syntactic grammar, this will make sense. Practical C++ implementations usually assume it well-formed and try. Nor do humans better here in essence.

The infix declarators of C can suffer in the similar way, too: the "right-left" rules will not work as the precise rules from the formal grammar when the to-be-declared identifier is unknown (due to the allowence to mix the abstract declarators and non-abstract declarators together as parts of a single declarator).

Such context-sensitivity and semantic-sensitivity is a result of the decision from the design of C declarators. Most other PLs use trailing/suffix syntaxes for type annotations in declarations (if any) all the time. The prefix/infix syntaxes in C is already a hack comparing to the tradition, and C++ has just added more hacks (to allow them coexisting) here.

In general, non-infix return syntaxes of explicit type annotation are more friendly to thinking in extrinsic typing:

On the opposite side, type inference exposes the duality as this process with a known set of typing rules, established by intrinsic typing design from a statically typed language. Type inference does not work in extrinsic typing thinking like this "in-place" way, as it transforms the potentially inferable in IR instead of terms directly represented in the source language. It is generally more difficult to perform this in human brains as it requires more memory (both for the IR representing typing environment and precise set of typing rules). Infix declaration is just one more difficult for this because the contextual noise will make every step a potentially non-trivial code transformation to keep the intermediate result always syntactically and semantically correct (consistent both to the the intended meaning of the original code and the language rules).

Moreover, although statically typed languages do not formally build the semantics for a typed program like this, the language itself is built (and evoluted) in this meta way, as the classical work from extending UTLC to STLC and languages with many more powerful type systems. An optimizing compiler for a dynamically typed (or more precisely, latent typed) language can also have the code transformation in any invented type systems not implied by the source language in the IR of its optimization passes on-the-fly, in essentially the same manner of extending a language to a more statically typed one by hand.

Despite to the meta-level techniques, the capability of being extrinsic is quite crucial for users already with experience of latent typing languages, esp. in the sense of teachability. Weakening the type annotation to a hint is the easiest way to make languages with different type disciplines handled in one same common way, without the requirements of knowing how to do code transformation in meta level (either typing algorithms in extrinsic typing, or type inference in intrinsic typing).

FrankHB commented 2 months ago
template<typename T>
decltype(std::declval<T&>() + std::declval<T&>()) fun(T x);

vs

template<typename T>
auto fun(T x) -> decltype(x + x);

Is auto necessary here? No, but it's arguably nicer.

Edit: changed function name to avoid confusion with the bar above.

I admit I am not a fan of trailing return type syntax of C++, and I come here just because I want to find some criteria of "nicer", before I can make some improvement to current clang-tidy's [fuchsia-trailing-return] which will kill all instances out of lambda-expressions and decltype contexts. This is far from "nice" (it even does not detect the necessity).