think-cell Suite è arrivata. Scopri la tua think-cell library e le nuove funzionalità.

Condividi

+ - 0:00:00
Notes for current slide
Notes for next slide

The C++ Rvalue Lifetime Disaster

by Arno Schoedl

aschoedl@think-cell.com

1 / 79

Rvalue References

  • STL advocates value semantics
  • lead to frequent copying in C++03
  • rvalue references invented to avoid copying
    • replaced by more efficent moving
2 / 79

Rvalue References

  • STL advocates value semantics
  • lead to frequent copying in C++03
  • rvalue references invented to avoid copying
    • replaced by more efficent moving
std::vector< std::vector<int> > vecvec;
std::vector<int> vec={1,2,3};
vecvec.emplace_back( std::move(vec) ); // rvalue reference avoids copy
3 / 79

Rvalue References

  • STL advocates value semantics
  • lead to frequent copying in C++03
  • rvalue references invented to avoid copying
    • replaced by more efficent moving
std::vector< std::vector<int> > vecvec;
std::vector<int> vec={1,2,3};
vecvec.emplace_back( std::move(vec) ); // rvalue reference avoids copy
  • Increasingly used to manage lifetime
    • C++11 std::cref
    • C++20 Ranges
4 / 79

Rvalue References

  • STL advocates value semantics
  • lead to frequent copying in C++03
  • rvalue references invented to avoid copying
    • replaced by more efficent moving
std::vector< std::vector<int> > vecvec;
std::vector<int> vec={1,2,3};
vecvec.emplace_back( std::move(vec) ); // rvalue reference avoids copy
  • Increasingly used to manage lifetime
    • C++11 std::cref
    • C++20 Ranges
auto rng=std::vector<int>{1,2,3} | std::ranges::view::filter([](int i){ return 0==i%2; });
// DOES NOT COMPILE
  • rng would contain dangling reference to std::vector<int>
  • So std::ranges::view::filter does not compile for rvalues
5 / 79

Rvalue References for Moving - Pitfalls

A foo() {
A const a=...;
...
return std::move(a);
};
  • What happens?
6 / 79

Rvalue References for Moving - Pitfalls

A foo() {
A const a=...;
...
return std::move(a);
};
  • What happens?
    • Copy - cannot move out of const
7 / 79

Rvalue References for Moving - Pitfalls

A foo() {
A const a=...;
...
return std::move(a);
};
  • What happens?
    • Copy - cannot move out of const
A foo() {
A a=...;
...
return std::move(a);
};
  • What happens?
8 / 79

Rvalue References for Moving - Pitfalls

A foo() {
A const a=...;
...
return std::move(a);
};
  • What happens?
    • Copy - cannot move out of const
A foo() {
A a=...;
...
return std::move(a);
};
  • What happens?
    • Move
    • Best we can do?
9 / 79

Rvalue References for Moving - Pitfalls

A foo() {
A a=...;
...
return a;
};
  • What happens?
10 / 79

Rvalue References for Moving - Pitfalls

A foo() {
A a=...;
...
return a;
};
  • What happens?
    • NRVO (Named Return Value Optimization) - copy/move elided
    • std::move can make things worse
11 / 79

Rvalue References for Moving - Pitfalls

A foo() {
A a=...;
...
return a;
};
  • What happens?
    • NRVO (Named Return Value Optimization) - copy/move elided
    • std::move can make things worse
A foo() {
A const a=...;
...
return a;
};
  • What happens?
12 / 79

Rvalue References for Moving - Pitfalls

A foo() {
A a=...;
...
return a;
};
  • What happens?
    • NRVO (Named Return Value Optimization) - copy/move elided
    • std::move can make things worse
A foo() {
A const a=...;
...
return a;
};
  • What happens?
    • Still NRVO (Named Return Value Optimization) - copy/move elided
13 / 79

Rvalue References for Moving - Pitfalls

A foo() {
if(... condition ...) {
A const a=...;
...
return a;
} else {
A const a=...;
...
return a;
}
};
  • What happens?
14 / 79

Rvalue References for Moving - Pitfalls

A foo() {
if(... condition ...) {
A const a=...;
...
return a;
} else {
A const a=...;
...
return a;
}
};
  • What happens?
    • No NRVO, returned object is not always same one
    • Copy because of const :-(
15 / 79

Rvalue References for Moving - Pitfalls

A foo() {
if(... condition ...) {
A a =...;
...
return a;
} else {
A a=...;
...
return a;
}
};
  • What happens?
    • Move
16 / 79

Rvalue References for Moving - Pitfalls

struct B {
A m_a;
};
A foo() {
B b=...;
...
return b.m_a;
};
  • What happens?
17 / 79

Rvalue References for Moving - Pitfalls

struct B {
A m_a;
};
A foo() {
B b=...;
...
return b.m_a;
};
  • What happens?
    • Copy
    • Members do not automatically become rvalues
18 / 79

Rvalue References for Moving - Pitfalls

struct B {
A m_a;
};
A foo() {
B b=...;
...
return std::move(b).m_a;
};
  • What happens?
19 / 79

Rvalue References for Moving - Pitfalls

struct B {
A m_a;
};
A foo() {
B b=...;
...
return std::move(b).m_a;
};
  • What happens?
    • Move
    • Member access of rvalue is rvalue
20 / 79

Rvalue References for Moving - Pitfalls

struct B {
A m_a;
};
A foo() {
B b=...;
...
return std::move(b).m_a;
};
  • What happens?
    • Move
    • Member access of rvalue is rvalue
  • Recommendations
    • Make return variables non-const
    • Use Clang's -Wmove
21 / 79

Temporary Lifetime Extension

struct A;
struct B {
private:
A m_a;
public:
A const& getA() const& { return m_a; }
};
B b;
auto const& a=b.getA();
22 / 79

Temporary Lifetime Extension

struct A;
struct B {
private:
A m_a;
public:
A const& getA() const& { return m_a; }
};
struct C {
A getA() const&;
};
B b;
C c;
auto const& a=< b or c >.getA();
23 / 79

Temporary Lifetime Extension

struct A;
struct B {
private:
A m_a;
public:
A const& getA() const& { return m_a; }
};
struct C {
A getA() const&;
};
B b;
C c;
auto const& a=< b or c >.getA();
  • auto const& a=c.getA(); works thanks to temporary lifetime extension
  • Idea: always write auto const&, the right thing happens
24 / 79

Temporary Lifetime Extension vs. Rvalues

bool operator<(A const&, A const&);
struct C {
A getA() const&;
} c1, c2;
auto const& a=std::min( c1.getA(), c2.getA() );
25 / 79

Temporary Lifetime Extension vs. Rvalues

bool operator<(A const&, A const&);
struct C {
A getA() const&;
} c1, c2;
auto const& a=std::min( c1.getA(), c2.getA() );
namespace std {
template<typename T>
T const& min( T const& lhs, T const& rhs ) {
return rhs<lhs ? rhs : lhs;
}
}
  • std::min forgets about rvalue-ness
  • a dangles
26 / 79

Temporary Lifetime Extension vs. Rvalues

bool operator<(A const&, A const&);
struct C {
A getA() const&;
} c1, c2;
auto const& a=our::min( c1.getA(), c2.getA() );
namespace our {
template<typename Lhs, typename Rhs>
decltype(auto) min( Lhs&& lhs, Rhs&& rhs ) {
return rhs<lhs ? std::forward<Rhs>(rhs) : std::forward<Lhs>(lhs);
}
}
  • our::min correctly returns A&&
27 / 79

Temporary Lifetime Extension vs. Rvalues

bool operator<(A const&, A const&);
struct C {
A getA() const&;
} c1, c2;
auto const& a=our::min( c1.getA(), c2.getA() );
namespace our {
template<typename Lhs, typename Rhs>
decltype(auto) min( Lhs&& lhs, Rhs&& rhs ) {
return rhs<lhs ? std::forward<Rhs>(rhs) : std::forward<Lhs>(lhs);
}
}
  • our::min correctly returns A&&
  • a still dangles
  • temporary lifetime extension does not keep rvalue references alive!
    • would only be possible by creating a copy
28 / 79

Temporary Lifetime Extension vs. decltype(auto)

A some_A();
- or -
A const& some_A();
  • forwarding return:
decltype(auto) foo() {
return some_A();
}
29 / 79

Temporary Lifetime Extension vs. decltype(auto)

A some_A();
- or -
A const& some_A();
  • forwarding return:
decltype(auto) foo() {
return some_A();
}
  • forwarding return with code in between:
??? foo() {
??? a = some_A();
... do something ...
return a;
}
30 / 79

Temporary Lifetime Extension vs. decltype(auto)

decltype(auto) foo() {
auto const& a = some_A();
... do something ...
return a;
}
31 / 79

Temporary Lifetime Extension vs. decltype(auto)

decltype(auto) foo() {
auto const& a = some_A();
... do something ...
return a;
}
  • creates dangling reference if some_A() returns value
32 / 79

Temporary Lifetime Extension vs. decltype(auto)

decltype(auto) foo() {
auto const& a = some_A();
... do something ...
return a;
}
  • creates dangling reference if some_A() returns value
auto foo() {
auto const& a = some_A();
... do something ...
return a;
}
33 / 79

Temporary Lifetime Extension vs. decltype(auto)

decltype(auto) foo() {
auto const& a = some_A();
... do something ...
return a;
}
  • creates dangling reference if some_A() returns value
auto foo() {
auto const& a = some_A();
... do something ...
return a;
}
  • always copies
34 / 79

Temporary Lifetime Extension vs. decltype(auto)

decltype(auto) foo() {
auto const& a = some_A();
... do something ...
return a;
}
  • creates dangling reference if some_A() returns value
auto foo() {
auto const& a = some_A();
... do something ...
return a;
}
  • always copies
  • Problem: temporary lifetime extension lies about its type
    • if some_A() returns value, a is really value, not reference
35 / 79

auto_cref

  • Deprecate temporary lifetime extension
  • Automatically declare variable
    • auto if constructed from value or rvalue reference, and
    • auto const& if constructed from lvalue reference
36 / 79

auto_cref

  • Deprecate temporary lifetime extension
  • Automatically declare variable
    • auto if constructed from value or rvalue reference, and
    • auto const& if constructed from lvalue reference
template<typename T>
struct decay_rvalues;
template<typename T>
struct decay_rvalues<T&> {
using type=T&;
};
template<typename T>
struct decay_rvalues<T&&> {
using type=std::decay_t<T>;
};
#define auto_cref( var, ... ) \
typename decay_rvalues<decltype((__VA_ARGS__))&&>::type var = ( __VA_ARGS__ );
37 / 79

auto_cref

decltype(auto) foo() {
auto_cref( a, some_A() );
... do something with a ...
return a;
}
38 / 79

auto_cref

decltype(auto) foo() {
auto_cref( a, some_A() );
... do something with a ...
return a; // no parentheses here!
}
39 / 79

auto_cref

decltype(auto) foo() {
auto_cref( a, some_A() );
... do something with a ...
return a; // no parentheses here!
}
  • Make it your default auto !
    • does not work yet if expression contains lambda, fixed in C++20
40 / 79

auto_cref

decltype(auto) foo() {
auto_cref( a, some_A() );
... do something with a ...
return a; // no parentheses here!
}
  • Make it your default auto !
    • does not work yet if expression contains lambda, fixed in C++20
  • Choice: auto_cref value const?

    template<typename T>
    struct decay_rvalues<T&&> {
    using type=std::decay_t<T> const;
    };
  • Then auto_cref_return for NRVO/move optimization

41 / 79

auto_cref

bool operator<(A const&, A const&);
struct C {
A getA() const&;
} c1, c2;
auto_cref( a, our::min( c1.getA(), c2.getA() ) );
namespace our {
template<typename Lhs, typename Rhs>
decltype(auto) min( Lhs&& lhs, Rhs&& rhs ) {
return rhs<lhs ? std::forward<Rhs>(rhs) : std::forward<Lhs>(lhs);
}
}
  • our::min correctly returns rvalue reference
  • auto_cref correctly turns it into value
42 / 79

C++ Rvalue Amnesia

struct A;
struct B {
A m_a;
};
auto_cref( a, B().m_a );
43 / 79

C++ Rvalue Amnesia

struct A;
struct B {
A m_a;
};
auto_cref( a, B().m_a );
  • Works
    • decltype((B().m_a)) is A&&
    • a is value
44 / 79

C++ Rvalue Amnesia

struct A;
struct B {
private:
A m_a;
public:
A const& getA() const {
return m_a;
}
};
auto_cref( a, B().getA() );
45 / 79

C++ Rvalue Amnesia

struct A;
struct B {
private:
A m_a;
public:
A const& getA() const {
return m_a;
}
};
auto_cref( a, B().getA() );
  • Does not work
    • decltype(B().getA()) is A const&
    • a is const&, dangles
46 / 79

C++ Rvalue Amnesia

struct A;
struct B {
private:
A m_a;
public:
A const& getA() const& {
return m_a;
}
};
auto_cref( a, B().getA() );
  • Does not work
    • decltype(B().getA()) is A const&
    • a is const&, dangles
47 / 79

C++ Rvalue Amnesia

struct A;
struct B {
private:
A m_a;
public:
A const& getA() const& {
return m_a;
}
};
auto_cref( a, B().getA() );
  • Does not work
    • decltype(B().getA()) is A const&
    • a is const&, dangles
  • Fundamental problem: const& binds anything, including rvalues
  • Affects any const& accessor
48 / 79

Conditional Operator Afraid Of Rvalue Amnesia

struct A;
A const& L();
A const&& R();
  • What is decltype( false ? L() : L() )?
    • A const&
  • What is decltype( false ? R() : R() )?
    • A const&&
49 / 79

Conditional Operator Afraid Of Rvalue Amnesia

struct A;
A const& L();
A const&& R();
  • What is decltype( false ? L() : L() )?
    • A const&
  • What is decltype( false ? R() : R() )?
    • A const&&
  • What is decltype( false ? R() : L() )?
50 / 79

Conditional Operator Afraid Of Rvalue Amnesia

struct A;
A const& L();
A const&& R();
  • What is decltype( false ? L() : L() )?
    • A const&
  • What is decltype( false ? R() : R() )?
    • A const&&
  • What is decltype( false ? R() : L() )?
    • A const
    • C++ forces a copy
51 / 79

C++20 common_reference Not Afraid

  • C++20 has new trait common_reference_t
    • invented for C++20 Ranges
52 / 79

C++20 common_reference Not Afraid

  • C++20 has new trait common_reference_t
    • invented for C++20 Ranges
  • std::common_reference_t< A const&, A const& > is
    • A const&
  • std::common_reference_t< A const&&, A const&& > is
    • A const&&
53 / 79

C++20 common_reference Not Afraid

  • C++20 has new trait common_reference_t
    • invented for C++20 Ranges
  • std::common_reference_t< A const&, A const& > is
    • A const&
  • std::common_reference_t< A const&&, A const&& > is
    • A const&&
  • std::common_reference_t< A const&&, A const& > is
54 / 79

C++20 common_reference Not Afraid

  • C++20 has new trait common_reference_t
    • invented for C++20 Ranges
  • std::common_reference_t< A const&, A const& > is
    • A const&
  • std::common_reference_t< A const&&, A const&& > is
    • A const&&
  • std::common_reference_t< A const&&, A const& > is
    • A const& !
55 / 79

C++20 common_reference Not Afraid

  • C++20 has new trait common_reference_t
    • invented for C++20 Ranges
  • std::common_reference_t< A const&, A const& > is
    • A const&
  • std::common_reference_t< A const&&, A const&& > is
    • A const&&
  • std::common_reference_t< A const&&, A const& > is
    • A const& !
  • std::common_reference_t< A const, A const& > is
56 / 79

C++20 common_reference Not Afraid

  • C++20 has new trait common_reference_t
    • invented for C++20 Ranges
  • std::common_reference_t< A const&, A const& > is
    • A const&
  • std::common_reference_t< A const&&, A const&& > is
    • A const&&
  • std::common_reference_t< A const&&, A const& > is
    • A const& !
  • std::common_reference_t< A const, A const& > is
    • A !
57 / 79

C++20 common_reference Not Afraid

  • C++20 has new trait common_reference_t
    • invented for C++20 Ranges
  • std::common_reference_t< A const&, A const& > is
    • A const&
  • std::common_reference_t< A const&&, A const&& > is
    • A const&&
  • std::common_reference_t< A const&&, A const& > is
    • A const& !
  • std::common_reference_t< A const, A const& > is
    • A !
  • std::common_reference embraces rvalue amnesia
58 / 79

C++20 common_reference Not Afraid

  • C++20 has new trait common_reference_t
    • invented for C++20 Ranges
  • std::common_reference_t< A const&, A const& > is
    • A const&
  • std::common_reference_t< A const&&, A const&& > is
    • A const&&
  • std::common_reference_t< A const&&, A const& > is
    • A const& !
  • std::common_reference_t< A const, A const& > is
    • A !
  • std::common_reference embraces rvalue amnesia

WHAT IS CORRECT?

59 / 79

Promises of References

Lifetime short long
Mutablity
immutable const&& const&
mutable && &
60 / 79

Promises of References

Lifetime short long
Mutablity
immutable const&& const&
mutable && &
[can scavenge]
61 / 79

Promises of References

Lifetime short long
Mutablity
immutable const&& -----> const&
->
^ / ^
| -- |
/
mutable && &
[can scavenge]
  • Current C++ reference binding strengthens lifetime promise
62 / 79

Promises of References

Lifetime short long
Mutablity
immutable const&& <----- const&
<-
^ \ ^
| -- |
\
mutable && &
[can scavenge]
  • Better: Allow binding only if promises get weaker

    • less lifetime
    • less mutability
    • less "scavenge-ability"
  • only lvalues should bind to const&

  • anything may bind to const&&
63 / 79

UUuuuuuuuh

  • This is so sad.
  • It is very sad.
  • We dug ourselves a hole.
  • And fell into it.
  • UUuuuuh.
64 / 79

Any Chance to Fix C++?

  • Warning: These are Ideas! Has not been Implemented!
65 / 79

Any Chance to Fix C++?

  • Warning: These are Ideas! Has not been Implemented!

  • Existing code must continue to work

  • Existing libraries must work with new code
    • gradual introduction of new binding rules within one codebase
66 / 79

Any Chance to Fix C++?

  • Warning: These are Ideas! Has not been Implemented!

  • Existing code must continue to work

  • Existing libraries must work with new code
    • gradual introduction of new binding rules within one codebase
  • Any reference uses either new or old rules
    • Reference binding only at beginning of reference lifetime
    • Type of resulting reference unchanged
67 / 79

Any Chance to Fix C++?

  • Warning: These are Ideas! Has not been Implemented!

  • Existing code must continue to work

  • Existing libraries must work with new code
    • gradual introduction of new binding rules within one codebase
  • Any reference uses either new or old rules
    • Reference binding only at beginning of reference lifetime
    • Type of resulting reference unchanged
  • All declarations inside #new-reference-binding on/off bind along new rules
auto const& a = ... // old rules apply
#new-reference-binding on
auto const& a = ... // new rules apply
#new-reference-binding off
auto const& a = ... // old rules apply
68 / 79

Reference Declarations (1)

  • local/global variable initialization

    auto const& a = ...
  • structured binding

    auto const& [a,b] = ...
  • function/lambda parameter lists

    void foo(A const& a);
69 / 79

Reference Declarations (2)

  • members (initialized in PODs)

    struct B {
    A const& m_a;
    } b = { a };
  • members (initialized in constructors)

    struct B {
    A const& m_a;
    B(A const& a) : m_a(a) {}
    };
  • lambda captures

    [&a = b]() { .... };
70 / 79

How to opt in to new behavior?

  • All declarations inside #new-reference-binding on/off bind along new rules
void A(int const& a);
#new-reference-binding on
void B(int const& a);
void C(int const&& a);
#new-reference-binding off
void B(int const& a) { // error: declared with different binding behavior
...
}
A(5); // compiles
B(5); // error: cannot bind rvalue to lvalue
C(5); // compiles
int a=1;
C(a); // compiles
71 / 79

Impact on Standard Library

  • Feature-test macro if #new-reference-binding is enabled
  • Functions can be implemented equivalently
    • typically replace const& parameters with const&&
  • <type_traits>
    • std::common_reference
    • others not affected
72 / 79

Until then... Mitigations (1)

  • temporary lifetime extension
    • replace by auto_cref
73 / 79

Until then... Mitigations (1)

  • temporary lifetime extension
    • replace by auto_cref
  • member accessors
    • delete rvalue accessors
    • macro?
struct B {
private:
A m_a;
public:
A const& getA() const& {
return m_a;
}
A const& getA() const&& = delete;
};
74 / 79

Until then... Mitigations (2)

  • common_reference
namespace our {
template<typename... Ts>
struct common_reference {
using oldtype=std::common_reference_t<Ts...>;
using type=std::conditional_t<
std::is_lvalue_reference<oldtype>::value &&
std::disjunction<std::is_rvalue_reference<Ts> ...>::value,
std::remove_reference_t<oldtype>&&,
oldtype
>;
};
}
75 / 79

Until then... Mitigations (3)

  • decltype( false ? R() : L() )?
    • A const
    • C++ forces a copy
76 / 79

Until then... Mitigations (3)

  • decltype( false ? R() : L() )?
    • A const
    • C++ forces a copy
  • our::common_reference allows fearless conditional (ternary) operator
#define CONDITIONAL(b, l, r) ( \
b \
? static_cast< typename our::common_reference<decltype((l)),decltype((r))>::type >(l) \
: static_cast< typename our::common_reference<decltype((l)),decltype((r))>::type >(r) \
)
77 / 79

Until then... Mitigations (3)

  • decltype( false ? R() : L() )?
    • A const
    • C++ forces a copy
  • our::common_reference allows fearless conditional (ternary) operator
#define CONDITIONAL(b, l, r) ( \
b \
? static_cast< typename our::common_reference<decltype((l)),decltype((r))>::type >(l) \
: static_cast< typename our::common_reference<decltype((l)),decltype((r))>::type >(r) \
)
  • decltype( CONDITIONAL( false, R(), L() ) )?
    • A const&&
    • no immediate copy
78 / 79

Summary

  • const& should never have bound to rvalues
  • Fixing C++ may be possible, but must demonstrate it
    • Clang implementation
    • large code base to try it on
  • Until then, consider mitigations

THANK YOU!

79 / 79

Rvalue References

  • STL advocates value semantics
  • lead to frequent copying in C++03
  • rvalue references invented to avoid copying
    • replaced by more efficent moving
2 / 79
Paused

Help

Keyboard shortcuts

, , Pg Up, k Go to previous slide
, , Pg Dn, Space, j Go to next slide
Home Go to first slide
End Go to last slide
b / m / f Toggle blackout / mirrored / fullscreen mode
c Clone slideshow
p Toggle presenter mode
t Restart the presentation timer
?, h Toggle this help
Esc Back to slideshow