Björn Fahller
June 13, 2019
130

StockholmCpp23 - Variant Visitation Variations

std::variant<> and std::visit() are arguably among the most important new features of C++17, allowing for the type safe flexibility provided by virtual functions, without the coupling from inheritance. But some say visit() is slow. Let's find out, and try a few different techniques to implement visit() and see what the standard says.

June 13, 2019

Transcript

1. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

1/50 Variant Visitation Variations Björn Fahller
2. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

2/50 <variant> std::variant<Ts...>
3. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

3/50 <variant> std::variant<Ts...> – In C++ 17
4. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

4/50 <variant> std::variant<Ts...> – In C++ 17 – Is a sum-type
5. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

5/50 <variant> std::variant<Ts...> – In C++ 17 – Is a sum-type using V = std::variant<A,B>; Instances of V can hold either the values from A, or the values from B. i.e., the value space of V is the sum of the value spaces of A and B.
6. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

6/50 <variant> std::variant<Ts...> – In C++ 17 – Is a sum-type – Is type safe
7. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

7/50 <variant> std::variant<Ts...> – In C++ 17 – Is a sum-type – Is type safe using V = std::variant<A,B>; V v{A{}};
8. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

8/50 <variant> std::variant<Ts...> – In C++ 17 – Is a sum-type – Is type safe using V = std::variant<A,B>; V v{A{}}; if (std::holds_alternative<A>(v)) func(std::get<A>(v));
9. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

9/50 <variant> std::variant<Ts...> – In C++ 17 – Is a sum-type – Is type safe using V = std::variant<A,B>; V v{A{}}; if (std::holds_alternative<A>(v)) func(std::get<A>(v)); std::get<B>(v); // throws
10. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

10/50 <variant> std::variant<Ts...> – In C++ 17 – Is a sum-type – Is type safe – Is a value type
11. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

11/50 <variant> auto std::visit(visitor, variants...) – Calls visitor with the actual types held by the variants – visitor must have overloads that spans the entire type space
12. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

12/50 <variant> auto std::visit(visitor, variants...) – Calls visitor with the actual types held by the variants – visitor must have overloads that spans the entire type space struct add {}; struct subtract {}; struct constant { int value; }; struct variable { std::string name; }; using expression = std::variant< add, subtract, constant, variable>; expression evaluate(expression e) { struct visitor { expression operator()(add) { … } expression operator()(subtract) { … } expression operator()(constant c) { … c.value; } expression operator()(variable v) { … lookup(v.name); } }; return std::visit(visitor{}, e); }
13. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

13/50 <variant> auto std::visit(visitor, variants...) – Calls visitor with the actual types held by the variants – visitor must have overloads that spans the entire type space struct add {}; struct subtract {}; struct constant { int value; }; struct variable { std::string name; }; using expression = std::variant< add, subtract, constant, variable>; expression evaluate(expression e) { struct visitor { expression operator()(add) { … } expression operator()(subtract) { … } expression operator()(constant c) { … c.value; } expression operator()(variable v) { … lookup(v.name); } }; return std::visit(visitor{}, e); } Note that the types in the variant are only conceptually related, they are not related in the type system.
14. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

14/50 <variant> auto std::visit(visitor, variants...) – Calls visitor with the actual types held by the variants – visitor must have overloads that spans the entire type space
15. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

15/50 <variant> Mateusz Pusz ACCU 2019 20 minutes https://www.youtube.com/watch?v=JGYxOieiZnY
16. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

16/50 <variant> Some say generated code is inefficient
17. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

17/50 <variant> Some say generated code is inefficient – Let’s have a look
18. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

18/50 <variant> Some say generated code is inefficient – Let’s have a look https://gcc.godbolt.org/z/51ufc6
19. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

19/50 <variant> Some say generated code is inefficient – Let’s have a look – Why the check for index==-1? https://gcc.godbolt.org/z/51ufc6
20. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

20/50 Throws: bad_­ variant_­ access if any variant in vars is valueless_­ by_­ exception(). http://eel.is/c++draft/variant.visit#4
21. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

21/50 http://eel.is/c++draft/variant.variant#variant.status 20.7.3.5 Value status [variant.status] constexpr bool valueless_by_exception() const noexcept; Effects: Returns false if and only if the variant holds a value. [ Note: A variant might not hold a value if an exception is thrown during a type-changing assignment or emplacement. The latter means that even a variant<float, int> can become valueless_­ by_­ exception(), for instance by struct S { operator int() { throw 42; }}; variant<float, int> v{12.f}; v.emplace<1>(S()); — end note ] Throws: bad_­ variant_­ access if any variant in vars is valueless_­ by_­ exception(). http://eel.is/c++draft/variant.visit#4
22. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

22/50 http://eel.is/c++draft/variant.variant#variant.status 20.7.3.5 Value status [variant.status] constexpr bool valueless_by_exception() const noexcept; Effects: Returns false if and only if the variant holds a value. [ Note: A variant might not hold a value if an exception is thrown during a type-changing assignment or emplacement. The latter means that even a variant<float, int> can become valueless_­ by_­ exception(), for instance by struct S { operator int() { throw 42; }}; variant<float, int> v{12.f}; v.emplace<1>(S()); — end note ] Throws: bad_­ variant_­ access if any variant in vars is valueless_­ by_­ exception(). http://eel.is/c++draft/variant.visit#4
23. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

23/50 http://eel.is/c++draft/variant.variant#variant.status 20.7.3.5 Value status [variant.status] constexpr bool valueless_by_exception() const noexcept; Effects: Returns false if and only if the variant holds a value. [ Note: A variant might not hold a value if an exception is thrown during a type-changing assignment or emplacement. The latter means that even a variant<float, int> can become valueless_­ by_­ exception(), for instance by struct S { operator int() { throw 42; }}; variant<float, int> v{12.f}; v.emplace<1>(S()); — end note ] Throws: bad_­ variant_­ access if any variant in vars is valueless_­ by_­ exception(). http://eel.is/c++draft/variant.visit#4
24. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

24/50 http://eel.is/c++draft/variant.variant#variant.status 20.7.3.5 Value status [variant.status] constexpr bool valueless_by_exception() const noexcept; Effects: Returns false if and only if the variant holds a value. [ Note: A variant might not hold a value if an exception is thrown during a type-changing assignment or emplacement. The latter means that even a variant<float, int> can become valueless_­ by_­ exception(), for instance by struct S { operator int() { throw 42; }}; variant<float, int> v{12.f}; v.emplace<1>(S()); — end note ] Throws: bad_­ variant_­ access if any variant in vars is valueless_­ by_­ exception(). http://eel.is/c++draft/variant.visit#4
25. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

25/50 http://eel.is/c++draft/variant.variant#variant.status 20.7.3.5 Value status [variant.status] constexpr bool valueless_by_exception() const noexcept; Effects: Returns false if and only if the variant holds a value. [ Note: A variant might not hold a value if an exception is thrown during a type-changing assignment or emplacement. The latter means that even a variant<float, int> can become valueless_­ by_­ exception(), for instance by struct S { operator int() { throw 42; }}; variant<float, int> v{12.f}; v.emplace<1>(S()); — end note ] Throws: bad_­ variant_­ access if any variant in vars is valueless_­ by_­ exception(). http://eel.is/c++draft/variant.visit#4
26. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

26/50 <variant> Some say generated code is inefficient – Let’s have a look – Why the check for index==-1? – Why calls through function pointers? https://gcc.godbolt.org/z/51ufc6
27. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

27/50 http://eel.is/c++draft/variant.visit#5 template<class Visitor, class... Variants> constexpr see below visit(Visitor&& vis, Variants&&... vars); 1. Let n be sizeof...(Variants)… … 5. Complexity: For n ≤ 1, the invocation of the callable object is implemented in constant time, i.e., for n = 1, it does not depend on the number of alternative types of Variants 0 . For n > 1, the invocation of the callable object has no complexity requirements.
28. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

28/50 http://eel.is/c++draft/variant.visit#5 template<class Visitor, class... Variants> constexpr see below visit(Visitor&& vis, Variants&&... vars); 1. Let n be sizeof...(Variants)… … 5. Complexity: For n ≤ 1, the invocation of the callable object is implemented in constant time, i.e., for n = 1, it does not depend on the number of alternative types of Variants 0 . For n > 1, the invocation of the callable object has no complexity requirements.
29. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

29/50 http://eel.is/c++draft/variant.visit#5 template<class Visitor, class... Variants> constexpr see below visit(Visitor&& vis, Variants&&... vars); 1. Let n be sizeof...(Variants)… … 5. Complexity: For n ≤ 1, the invocation of the callable object is implemented in constant time, i.e., for n = 1, it does not depend on the number of alternative types of Variants 0 . For n > 1, the invocation of the callable object has no complexity requirements.
30. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

30/50 <variant> Some say generated code is inefficient – Let’s have a look – Why the check for index==-1? – Why calls through function pointers? – Let’s break the rules! https://gcc.godbolt.org/z/51ufc6
31. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

31/50 A non-conforming visit() template < typename Vi, typename Va, size_t idx = std::variant_size_v<std::remove_reference_t<Va>> - 1 > auto visit(Vi&& vi, Va&& va) { if (idx == va.index()) { return std::invoke( std::forward<Vi>(vi), std::get<idx>(std::forward<Va>(va)) ); } if constexpr (idx > 0) { return visit<Vi, Va, idx - 1>( std::forward<Vi>(vi), std::forward<Va>(va) ); } else throw std::bad_variant_access(); }
32. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

32/50 template < typename Vi, typename Va, size_t idx = std::variant_size_v<std::remove_reference_t<Va>> - 1 > auto visit(Vi&& vi, Va&& va) { if (idx == va.index()) { return std::invoke( std::forward<Vi>(vi), std::get<idx>(std::forward<Va>(va)) ); } if constexpr (idx > 0) { return visit<Vi, Va, idx - 1>( std::forward<Vi>(vi), std::forward<Va>(va) ); } else throw std::bad_variant_access(); }
33. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

33/50 template < typename Vi, typename Va, size_t idx = std::variant_size_v<std::remove_reference_t<Va>> - 1 > auto visit(Vi&& vi, Va&& va) { if (idx == va.index()) { return std::invoke( std::forward<Vi>(vi), std::get<idx>(std::forward<Va>(va)) ); } if constexpr (idx > 0) { return visit<Vi, Va, idx - 1>( std::forward<Vi>(vi), std::forward<Va>(va) ); } else throw std::bad_variant_access(); }
34. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

34/50 template < typename Vi, typename Va, size_t idx = std::variant_size_v<std::remove_reference_t<Va>> - 1 > auto visit(Vi&& vi, Va&& va) { if (idx == va.index()) { return std::invoke( std::forward<Vi>(vi), std::get<idx>(std::forward<Va>(va)) ); } if constexpr (idx > 0) { return visit<Vi, Va, idx - 1>( std::forward<Vi>(vi), std::forward<Va>(va) ); } else throw std::bad_variant_access(); }
35. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

35/50 template < typename Vi, typename Va, size_t idx = std::variant_size_v<std::remove_reference_t<Va>> - 1 > auto visit(Vi&& vi, Va&& va) { if (idx == va.index()) { return std::invoke( std::forward<Vi>(vi), std::get<idx>(std::forward<Va>(va)) ); } if constexpr (idx > 0) { return visit<Vi, Va, idx - 1>( std::forward<Vi>(vi), std::forward<Va>(va) ); } else throw std::bad_variant_access(); }
36. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

36/50 template < typename Vi, typename Va, size_t idx = std::variant_size_v<std::remove_reference_t<Va>> - 1 > auto visit(Vi&& vi, Va&& va) { if (idx == va.index()) { return std::invoke( std::forward<Vi>(vi), std::get<idx>(std::forward<Va>(va)) ); } if constexpr (idx > 0) { return visit<Vi, Va, idx - 1>( std::forward<Vi>(vi), std::forward<Va>(va) ); } else throw std::bad_variant_access(); }
37. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

37/50 template < typename Vi, typename Va, size_t idx = std::variant_size_v<std::remove_reference_t<Va>> - 1 > auto visit(Vi&& vi, Va&& va) { if (idx == va.index()) { return std::invoke( std::forward<Vi>(vi), std::get<idx>(std::forward<Va>(va)) ); } if constexpr (idx > 0) { return visit<Vi, Va, idx - 1>( std::forward<Vi>(vi), std::forward<Va>(va) ); } else throw std::bad_variant_access(); } http://tiny.cc/sxaw7y cppinsights.io
38. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

38/50 template < typename Vi, typename Va, size_t idx = std::variant_size_v<std::remove_reference_t<Va>> - 1 > auto visit(Vi&& vi, Va&& va) { if (idx == va.index()) { return std::invoke( std::forward<Vi>(vi), std::get<idx>(std::forward<Va>(va)) ); } if constexpr (idx > 0) { return visit<Vi, Va, idx - 1>( std::forward<Vi>(vi), std::forward<Va>(va) ); } else throw std::bad_variant_access(); } http://tiny.cc/sxaw7y cppinsights.io https://gcc.godbolt.org/z/0mtCdY
39. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

39/50 <variant> Is visit() slow?
40. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

40/50 <variant> Is visit() slow?
41. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

41/50 <variant> Is visit() slow? – Not really
42. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

42/50 <variant> Is visit() slow? – Not really, but it’s not as fast as it could be.
43. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

43/50 <variant> Is visit() slow? – Not really, but it’s not as fast as it could be. – It’s surprisingly easy to write a fast(er) visit.
44. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

44/50 <variant> Is visit() slow? – Not really, but it’s not as fast as it could be. – It’s surprisingly easy to write a fast(er) visit. – The performance guarantees from the standard actually hurts the performance.
45. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

45/50 <variant> Is visit() slow? – Not really, but it’s not as fast as it could be. – It’s surprisingly easy to write a fast(er) visit. – The performance guarantees from the standard actually hurts the performance. – LLVMs if-else sequence to jump table optimization makes a non-complying implementation compliant!
46. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

46/50 Resources
47. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

47/50 Resources “Variant Visitation V2” - Michael Park https://mpark.github.io/programming/2019/01/22/variant-visitation-v2/
48. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

48/50 Resources “Variant Visitation V2” - Michael Park https://mpark.github.io/programming/2019/01/22/variant-visitation-v2/ “When performance guarantees hurts performance – std::visit” - Björn Fahller https://playfulprogramming.blogspot.com/2018/12/when-performance-guarantees-hu rts.html
49. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

49/50 Resources “Variant Visitation V2” - Michael Park https://mpark.github.io/programming/2019/01/22/variant-visitation-v2/ “When performance guarantees hurts performance – std::visit” - Björn Fahller https://playfulprogramming.blogspot.com/2018/12/when-performance-guarantees-hu rts.html “CPPInsights.io” - Andreas Fertig https://cppinsights.io
50. Variant Visitation Variations – StockholmCpp 23 © Björn Fahller @bjorn_fahller

50/50 Björn Fahller Variant Visitation Variations [email protected] @bjorn_fahller @rollbear