Coding standard for lambda trailing return types

I’m having a bit of a discussion with @avl-llvm on ⚙ D86539 [Debuginfo][llvm-dwarfutil] llvm-dwarfutil dsymutil-like tool for ELF. about whether lambdas should have their trailing return types when not required by the compiler. I see we don’t have an explicit coding standard detailing this, so I would like to suggest we create one. I see three possible policies, and would like feedback on which we adopt:

  1. Never unless forced to e.g. due to their not being one single type being returned (e.g. an Error in some places and a value in other places when wanting an Expected). Pros: least additional typing (so may help readability?), reasonably easy to enforce, avoids ugly trailing return types… Cons: not always immediately obvious what the return type actually is.
  2. Always. Pros: easiest to enforce as there’s no decision needed, may help understanding the code due to type being explicit? Cons: extra typing, and is usually unnecessary. A possible minor variation might be always except for void (since the lack of a returned value makes it obvious).
  3. Something similar to our rules on auto: where the return type isn’t obvious. Pros: consistency with auto. Cons: subjectivity involved - when is a return type non-obvious? In the case of auto it’s fairly easy to craft a set of rules where it isn’t obvious (i.e. use in place of iterator types or where the type is already mentioned on the right, e.g. due to a constructor or cast).

I advocate 1. In general, I think the return type of a lambda is basically always irrelevant to the maintenance of the code, because most lambdas are passed directly to a function at which point the function signature indicates the return type. The compiler (and indeed often things like Visual Studio’s intellisense) will tell you when your return types within the lambda don’t match or when the calculated return type isn’t compatible with the function you pass it into. If you are using a lambda as a way of avoiding code duplication, its return type will usually be passed to a new or existing variable, which won’t be auto as per our existing rules, so the type is obvious then.

Thoughts?

I don’t have a super strong personal opinion - but I lean towards 3. Sometimes it makes the code clearer and I don’t think it should be banned, yes it adds subjectivity - but we already have that for auto and AFAIK we haven’t see a ton of disagreement around that?

This also mirrors the Google and Ubisoft style guides.

I’m struggling to envisage cases where it would make the code clearer. Do you have any examples that would help? Those examples might also help us tease out some rules that are similar to the rules for auto if we choose to go with option 3.

My opinion is that in most cases, the returning type of lambda being written explicitly simplifies reading lambda. That is because, it is necessary to understand a single bit of information. While otherwise, it is necessary to understand more. f.e.

[&](raw_ostream &OutFile) -> Error {
  return objcopy::executeObjcopyOnBinary(Config, InputFile, OutFile);
}

If returning type is explicit then someone needs to check “-> Error” and that`s all.


[&](raw_ostream &OutFile) {
  return objcopy::executeObjcopyOnBinary(Config, InputFile, OutFile);
}

If returning type is implicit then someone needs to search for the declaration of objcopy::executeObjcopyOnBinary and then(if no other type conversions) returning type is understood. It is not too complex but still extra work.

examples where I think it is better to have an explicit returning type:

[&]() -> Error {
 ...
}

[&]() -> bool {
 ...
}

[&]() -> ClassName {
 ...
}

[&]() -> llvm::Optional<unsigned> {
 ...
}

examples where returning type makes things harder to read:

[&]() -> const std::vector<DWARFDebugRangeList::RangeListEntry>& {
 ...
}

[&]() -> IntervalMap<KeyT, ValT, IntervalMapImpl::NodeSizer<KeyT, ValT>::LeafSize,
                IntervalMapHalfOpenInfo<KeyT>> {
 ...
}

Thus, from my point of view, Having some variant of 3 would be good. Probably we could create some restrictions: always specify returning type unless it is greater than 30 symbols. Or another variant - someone can omit the returning type of lambda if it looks too massive.

1 Like

I also lean towards 3, but I don’t think focusing on whether the return type is obvious is necessarily what we want. I think the question is, if the return type is not obvious, whether knowing the return type helps with understanding the lambda (sometimes good naming is enough).

I’m inclined to agree with you. The situation I envisage is that it’s usually not necessary to know what the return type is explicitly (it’s more that we don’t care what the type is than it being unobvious). There could be a number of reasons for this. I thought of a couple off the top of my head:

  1. We are using the lambda as a predicate or callback function for another function. Sometimes, if the lambda is likely to require regular changes, having the return type might help to ensure we don’t have to keep updating things, but I suspect in most cases, the thing it’s passed into clearly indicates what it should be (for example a standard algorithm like std::find_if).
  2. The lambda is just a wrapper around another function, perhaps to insert some fixed arguments or similar. In this case, the return type just wants to be whatever the wrapped function returns. We don’t need the explicit return type on the lambda any more than we need the return type of the function at the lambda’s point of definition, in my opinion.