Support std::identity
What does this MR do?
This is an approved struct
for the C++20 standard. The implementation comes from the suggested implementation in cppreference.com.
Can this MR be accepted?
-
Implemented -
Added test -
Pipeline passing -
Added entry to CHANGELOG.md
Related issues
Merge request reports
Activity
mentioned in merge request pdelab/dune-pdelab!427 (closed)
In my opinion this code is really dangerous. As far as I understand, this will bind a reference to an outlived temporary if you do:
struct Foo {...}; auto&& foo = std::identity(Foo());
In this situation
T
is deduced to be plainFoo
and the created temporary is bound to the argument ofoperator()
which is an r-value reference of typeT&&
akaFoo&&
. This is OK and prolongs lifetime of the temporary as long as this reference lives. Nowstd::forward
casts this to an r-value referenceT&&
which is returned. The problem is that the lifetime of the object ends with the first bound-to reference which is theoperator()
argument. Hence, thefoo
variable outside will point to a non-existing object.I looked this up and also found the proposed implementation in the ranges TS. If this is really the desired behaviour it should be carefully documented that you're (surprisingly) not allowed to use r-values as arguments. In this case I explicitly ask you to add the corresponding documentation.
All of this holds true unless there was a language change in C++20 making this valid. In this case the return type should better be
T
here, which would avoid the outlined problem in pre C++20.The current (final before ISO member feedback) C++20 draft states in
20.14.11
:Effects: Equivalent to: return std::forward(t);
So this seems to be intended behavior.
It definitely is dangerous, it seems to be built mostly for direct consumption in an outer function call, where it works correctly, e.g.:
#include <iostream> struct Foo { Foo() { std::cout << "construct" << std::endl; } Foo(const Foo&) { std::cout << "copy construct" << std::endl; } Foo(Foo&&) { std::cout << "move construct" << std::endl; } ~Foo() { std::cout << "deconstruct" << std::endl; } }; struct identity { template<class T> constexpr T&& operator()(T&& t ) const noexcept {return std::forward<T>(t);} }; template<typename T> T&& print(T&& arg) { std::cout << "print" << std::endl; return std::forward<T>(arg); } int main() { auto id = identity(); print(id(Foo())); std::cout << "next" << std::endl; }
This prints:
construct
print
deconstruct
next
, so there is no problem with the lifetime in this context.Edited by Steffen Müthing
I just checked this against
g++-8.3 -std=c++17
. Usingstruct Foo { Foo() { std::cout << "construct" << std::endl; } Foo(const Foo&) { std::cout << "copy construct" << std::endl; } Foo(Foo&&) { std::cout << "move construct" << std::endl; } ~Foo() { std::cout << "deconstruct" << std::endl; } }; ... auto id = identity(); const auto& foo = id(Foo()); std::cout << "next" << std::endl;
will print 'construct', 'destruct', 'next' as expected.
The use case proposed by @smuething is OK and seems to be what it was invented for. In general passing r-value references should not be any problem. If the object lives on the stack, it will always outlive any reference obtained by
operator()
unless you explicitly shot yourself in the foot.As mentioned above, the problem is passing r-values and not r-value references. Coming with the ranges TS, the main purpose seems to be to act as a predicate in range algorithms. Unfortunately this may also lead to delicate problems with generated on the fly ranges. While this one is OK
auto id = identity; for(auto&& value : range) std::cout << value << std::endl;
the more verbose one is not
auto id = identity; for(auto it = range.begin(); it != range.end(); ++it) { auto&& value = id(*it); std::cout << value << std::endl; }
Although its generic name may indicate something else, this really is an expert utility and we shoud in detail document, what's allowed and what's not to avoid people running into hard to detect errors.
Maybe it's really a good idea to document the issue with a test:
struct Foo { static int count; Foo() { ++count; } Foo(const Foo&) { ++count; } Foo(Foo&&) { ++count } ~Foo() { --count; } }; int Foo::count = 0; ... auto id = identity(); const auto& foo = id(Foo()); if (Foo::count > 0) std::cerr << "Some meaningful message." << std::endl;
@smuething Sorry I misread your example. You are passing an r-value, but don't store the return value of
operator()
. That's still OK, because the object will live during the evaluation of the current expression.@carsten.graeser Alright, how about now?
- Resolved by Santiago Ospina De Los Ríos
@smuething @carsten.graeser Is this ready to merge? Or Post 2.7?
mentioned in commit 9bb1b60a