Nov 27, 2018

[C++][cppcon 2018] How to Write Well-Behaved Value Wrappers - Simon Brand


Value Wrappers

Types with value-semantics which can store objects of any type.
Do what 'int' does.
i.e
std::pair
std::optional
std::variant


Traits that value wrappers should have

  • Performant
  • Unsurprising


Hey, beware of code's 'hot paths'!


Let's start:

Comparison Operators

Weak ordering, i.e equivalence rather than equality.
Strong ordering, i.e equality rather than equivalence.

Relation strength
(a <=> b) < 0  //true if a < b
(a <=> b) > 0  //true if a > b
(a <=> b) == 0 //true if a is equal/equivalent to b


spaceship operator returns types as follows:
Five types are provided, and stronger relations can implicitly convert to weaker ones:

(figure credits: https://blog.tartanllama.xyz/spaceship-operator/ )


Why those types?
1. Indicates to the user what kind of relation is modeled by the comparisons.
2. Algorithms takes advantage of being optimized.
i.e If the operands are compared equal with strongly-ordered type, it indicates
that any function taking either operand should give out the same result, thus
call the function once with a operand would be enough.
3. These types can be used to define language features, i.e
Class Types in Non-Type Template Parameters
( http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0732r0.pdf ),
i.e reduce template instantiation if the non-value type argument is strongly-ordered equal.

spaceship operator doesn't need to be free function like binary operators:
struct foo {
  int i;

  std::strong_ordering operator<=> (foo const& rhs) {
    return i <=> rhs.i;
  }
};

Use std::compare_three_way to fall back to two-way comparisons if there's
no three-way comparisons available.

Write a three-way comparisons operator for std::pair:
template<class T, class U>
struct pair {
  T t;
  U u;

  auto operator<=> (pair const& rhs) const
    -> std::common_comparison_category_t<
         decltype(std::compare_three_way{}(t, rhs.t)),
         decltype(std::compare_three_way{}(u, rhs.u)> {
    if (auto cmp = std::compare_three_way{}(t, rhs.t); cmp != 0) return cmp;
    return std::compare_three_way{}(u, rhs.u);
  }

std::common_comparison_category_t determines the weakest relation
in it's arguments.
i.e
std::common_comparison_category_t<std::strong_ordering, std::partial_ordering>
is std::partial_ordering

C++20 supports automatic generation of comparison operators.
auto operator<=>(x const&) = default;

Reference:
Implementing the spaceship operator for optional
Spaceship Operator
https://en.cppreference.com/w/cpp/header/compare


'noexcept' Propagation

Propagate the noexcept-ness of 'move' and 'swap' operations.
Why? Cause std containers honor noexcept 'move' constructor.


'explicit' or not 'explicit'

// OK but with code duplication.
template<typename U, std::enable_if_t<
    std::is_constructible<T, U>::value &&
    std::is_convertible<U, T>::value>* = nullptr>
wrapper(U&& u) : t(std::forward<U>(u)) {}


template<typename U, std::enable_if_t<
    std::is_constructible<T, U>::value &&
    !std::is_convertible<U, T>::value>* = nullptr>
explicit wrapper(U&& u) : t(std::forward<U>(u)) {}
to:
template<typename U, std::enable_if_t<
    std::is_constructible<T, U>::value>* = nullptr>
explicit(std::is_convertible<U, T>::value>)   // https://en.cppreference.com/w/cpp/language/explicit C++20
wrapper(U&& u) : t(std::forward<U>(u)) {}


Conditionally deleting special members

once touches the Rule of 5, it's zero-sum situation.
Use base class for rule of 5's delete/default and use private inherits.
Use concept.
optional(optional const& rhs) {
    if (rhs.enaged) {
        new (std::addressof(t)) T (rhs.t);
        engaged = true;
    }
}


Triviality propagation

'An object with either a non-trivial copy constructor or a non-trivial destructor
cannot be passed by value because such objects must have well defined addresses.'
This effects RVO.
We cannot use SFINAE on destructor/copy constructor.
However, we can again use base type and with private inherits.
Use concept to simplify the code.


Ref-qualified accessor functions:
template<typename Self>
decltype(auto) operator*(this Self&& self) {
    return std::forward<Self>(self).m_value;
}


SFINAE-unfriendly callables

Expression SFINAE: https://stackoverflow.com/a/12654277
consteval in C++20
SFINAE only works at:
quoted:
--
Only the failures in the types and expressions in the immediate context of the function type or its template parameter types or its explicit specifier (since C++20) are SFINAE errors.

If the evaluation of a substituted type/expression causes a side-effect such as instantiation of some template specialization, generation of an implicitly-defined member function, etc, errors in those side-effects are treated as hard errors.

A lambda expression is not considered part of the immediate context. (since C++20)
--
Be ware of SFINAE's hard errors.
#include <iostream>
#include <memory>

using namespace std;

struct foo {
    void do_thing()
    {
        cout << "do thing" << endl;
    }
};

template <typename T>
struct wrapper {
    T t;
    template <typename F>
    auto pass_to(F f) -> decltype(f(t)) // expression SFINAE check
    {
        f(t);
        cout << "no const" << endl;
    }

    template <typename F>
    auto pass_to(F f) const -> decltype(f(t)) // expression SFINAE check
    {
        f(t);
        cout << "const" << endl;
    }

    template <typename F>
    auto pass_to_no_decltype(F f)
    {
        f(t);
        cout << "no const" << endl;
    }

    template <typename F>
    auto pass_to_no_decltype(F f) const
    {
        f(t);
        cout << "const" << endl;
    }
};


int main()
{
    const wrapper<foo> f{foo{}};
    // hard error; error: 'this' argument to member function 'do_thing' has type 'const foo', but function is not marked const
    // f.pass_to([](auto &&x) { x.do_thing(); }); 

	// hard error;  error: 'this' argument to member function 'do_thing' has type 'const foo', but function is not marked const
    // f.pass_to([](auto &x) { x.do_thing(); });  

    f.pass_to([](auto x) { x.do_thing(); });

    // Using const this to differentiate.
    f.pass_to_no_decltype([](auto &&x) { x.do_thing(); });
    f.pass_to_no_decltype([](auto &x) { x.do_thing(); });
    f.pass_to_no_decltype([](auto x) { x.do_thing(); });
}

Simon uses 'this Self&& self' signature,
i.e take advantage of re-appearing the
'this' pointer(r-value), like in Python, to solve the problem,
due to 'this' is c.v qualified.
template<typename T>
struct wrapper {
    T t;
    template<typename Self, typename F>
    auto pass_to (this Self&& self, F f) -> decltype(f(self.t)) {
        f(t);
    }
};

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.