2

Cpp Core Guideline F19 tells us

Flag a function that takes a TP&& parameter (where TP is a template type parameter name) and does anything with it other than std::forwarding it exactly once on every static path.

Literally, that means something like

template<typename R, typename V> bool contains(R&& range, const V& value)
{
  return std::find(range, value) != range.end();
}

Should be flagged because std::forward was not called (and even if it was called, it'd call .end()).

But to my understanding, this contains-implementation is correct and sensible. I need the universal reference R&& because in real world, I need it to bind to const& values (1, see below) but I also need it to bind to values that cannot become const (because begin() and end() aren't const) (2). If I'd change the signature to contains(const R& range, const V& value), it wouldn't be compatible with most views.

int main()
{
  const std::vector<int> vs{1, 2, 3, 4};
  std::cout << contains(vs, 7);  // (1)
  std::cout << contains(vs | std::views::transform(squared), 7);  // (2)
}

Question: Does F19 miss some edge case or am I missing some detail? The std::contains possible implementation also passes R as a universal reference without calling std::forward.

Alternative contains implementation using std::forward (but also .end(), hence not compliant with F19, either):

template<typename R, typename V> bool contains(R&& range, const V& value)
{
  const auto& end = range.end();
  return std::find(std::forward<R>(range), value) != end;
}
7
  • Yes, this rule sounds like nonsense. Core guidelines are not perfect. Commented Jul 23, 2024 at 7:46
  • 1
    It seems this rule is specifically about the following case: If the object is to be passed onward to other code and not directly used by this function, we want to make this function agnostic to the argument const-ness and rvalue-ness.. Though I don't see any rule describing the case you want - function ignoring const-ness but using the value. Commented Jul 23, 2024 at 7:56
  • 6
    The guideline is not wrong, there are cases where this rvalue to lvalue cast would be a bug, your code is not an example of such code, if you follow the guidelines you might reduce the bugs, but not following them doesn't necessitate bugs, that's why they are guidelines, not language standard rules. Commented Jul 23, 2024 at 8:13
  • Are you sure your code compiles? Commented Jul 23, 2024 at 9:07
  • 2
    what would you add to the guideline so it does not match your case but still matches a case where one simply forgot a std::forward ? As others stated, the guideline is not about ruling out any code that does not meet the guideline but to help diagnose common mistakes by means of the guideline (and doing that necessarily involves false positives) Commented Jul 23, 2024 at 9:19

1 Answer 1

3

Yes.

Arguably the problem here is that forwarding references were originally introduced to serve one specific purpose: forwarding. That's why we ended up calling them forwarding references. In that context, it certainly makes sense to question using a forwarding reference that isn't forwarded. And indeed this is by far the most common usage of forwarding references.

However, forwarding references also end up serving a different purpose - one more inline with their original name: a universal reference. If you want to write a function that can accept either an lvalue or an rvalue today, you only have three choices:

  • template <class T> void f(T a); - this may be what you want, but it requires copying lvalues and forces a move of xvalues.
  • template <class T> void g(T const& b); - this is a good default choice, no move or copy happens, but b is always const
  • template <class T> void h(T&& c); - no move or copy happens, and c can be mutable

If you want to avoid a copy/move and you need to preserve const-ness, the only option is to use a forwarding reference. Which in this context is better named a universal reference. But it's the same syntax either way. Ranges make use of this pattern heavily - and there's nothing wrong with the pattern. It's the tool available at our disposal to solve this problem.

You might imagine that maybe if we had something like a parameter label, we might be able to differentiate between the contexts where we just want to have a universal reference and where we want to actually forward. But barring something like that, the "universal reference" use-case of forwarding references is perfectly valid. We just cannot syntactically distinguish between the cases where we intended to forward but forgot to (which should be flagged) and the cases where we did not intend to forward, and should not (which should not be flagged).

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.