Sep 5, 2022

[C++][Concept] Techniques

Referernce:
https://www.foonathan.net/2021/07/concepts-structural-nominal/
https://en.wikipedia.org/wiki/Structural_type_system
https://en.wikipedia.org/wiki/Nominal_type_system
C++ concepts by Sandor Dargo

This note extract the technique mentioned in Nathan's article for future quick reference.
Read https://vsdmars.blogspot.com/2022/03/c20-concepts-wrap-up.html first to have a good understanding about C++20 concept and how it works.

Go uses Interface to forge the concept of traits;
C++ relies on compile time thus using existing predicates  to check for well-formed syntax.

std concepts headers


std concepts library



Techniques

Technique 1

Create a concept to check Type's internal typedef; iff defined does this type works as we expected.
// concept definition ===//
template <typename T>
concept my_concept
  = requires { typename T::my_concept_tag; }
  && …;

//=== concept modelling ===//
struct my_type_modelling_the_concept
{
    using my_concept_tag = void; // Doesn't matter.
};
In std e.g. we have std::range::enable_view variable template;
thus we could explicit specialize it to enable 'view'' trait.
e.g.
namespace std
{
    // Tell the compiler that your view is a view.
    template <>
    constexpr bool enable_view<my_namespace::MyViewType> = true;
}
Consider above code is too verbose, we could simply inherit std::range::view_base to enable the 'view' trait'
This is due to in std we have enable_view defined as:
namespace std
{
    struct view_base
    {};

    // By default, a type is a view iff it inherits from view_base.
    template <typename T>
    constexpr bool enable_view = std::is_base_of_v<view_base, T>;
}

Technique 2

This technique is basically the previous I just mentioned; 
i.e. using inheritance type check.
//=== concept definition ===//
struct my_concept_base {};

template <typename T>
constexpr bool enable_my_concept
  = std::is_base_of_v<my_concept_base, T>;

template <typename T>
concept my_concept = enable_my_concept<T>
  && requires (T obj) { … };

//=== concept modelling ===//
struct my_type_modelling_the_concept : my_concept_base
{
  …
};

Technique 3

Opt-out a trait with specialize concept:
namespace std
{
    // MyLinkedList has O(n) size.
    template <typename T>
    constexpr bool std::ranges::disable_sized_range<MyLinkedList<T>> = true;
}
Opt-out a trait with variable template:
template <typename T>
constexpr bool disable_my_concept = false;

template <typename T>
concept my_concept = !disable_my_concept<T>
  && requires (T obj) { … };
We can use the same inheritance trick to opt-out a trait but that is counter-intuitive.


CppCoreGuidelines


#include <concepts>
template <typename T>
concept number = std::integral<T> || std::floating_point<T>;


template <typename T>
requires number<T>
auto add(T a, T b) {
	return a+b;
}

template <typename T>
auto add(T a, T b) requires number<T> {
	return a+b;
}

template <number T>
auto add(T a, T b) {
return a+b;
} 



Use a concept that takes multiple parameters


template<std::same_as<int>, T>
auto foo(T a) { // ...
}

template<typename U>
struct Fun{
  template<typename T, std::same_as<T> U>
  auto foo(T a) {
    // ...
  }
};
template <typename T>
concept number = std::integral<T> || std::floating_point<T>;

template <number T, std::same_as<T> U>
class WrappedNumbers { ... };
Most brevity:
auto add(number auto a, number auto b) {
  return a+b;
}

This works but it's hard to tell `number` is a type or concept.

auto add(number a, number b) {
}

Complete solution:

concept number = std::integral<T> || std::floating_point<T>;

auto add(number auto a, number auto b) {
  return a+b;
}


Use of delete to control API exposure

template <typename Key>
class Ignition {
public:
void insertKey(Key key) {
};

void insertKey(Key key) requires smart<Key> = delete;

};


OK:
template <typename Key> 
class Ignition {
  public:
  void start(Key key) { // ... }
  void start(Key key) requires smart<Key> {
  // ... 
  } 
}; 


Use concept for template type's member function

template <typename T>
class WrappedNumber {
public:
  T foo() requires std::floating_point<T> {
   // ...
  }

  T foo() requires std::integral<T> {
   // ...
  }

  T foo() requires std::integral<T> && std::is_signed_v<T> {
   // ...
  }
};


Constrain with delete

template <typename Key>
class Ignition {
 public:
 void insertKey(Key key) {
 // ...
 };

 void insertKey(Key key) requires smart<Key> = delete;
};


CppCoreGuidelines


No good
template <typename Key>
class Ignition {
 public:
 void start(Key key) requires !smart<Key> {
 // ...
 }

 void start(Key key) requires smart<Key> {
 // ...
 }
};

OK
template <typename Key>
class Ignition {
 public:
 void start(Key key) {
 // ...
 }

 void start(Key key) requires smart<Key> {
 // ...
 }
};


Std Headers

<concepts>

std::convertible_to for conversions with fewer surprises

template <typename T>
 void fun(T bar) requires std::convertible_to<T, bool> {
 std::cout << std::boolalpha << static_cast<bool>(bar) << '\n';
}

int main() {
 fun(5); // OK an int can be converted into a boolean
 // fun(std::string("Not OK"));
 // void fun(T) requires convertible_to<T, bool>
 // [with T = std::__cxx11::basic_string<char>]' with unsatisfied constraints
}


std::totally_ordered for defined comparisons

std::totally_ordered helps to accept types that specify all the 6 comparison operators
(==, !=, <, >, <=, >=) and that the results are consistent with a strict total order on T.
Reference:
https://vsdmars.blogspot.com/2023/10/c20-nicolai-m-josuttiss-c20-complete.html


The strict total order of items in math means that having a, b and c coming from the same set, all
satisfy the following requirements:
  • Not a < a // Irreflexive
  • if a < b and b < c then a < c // Transitive
  • if a != b then a < b or b < a // Connected
A type without the comparison operators defined is not totally ordered, while a type that defines
those operators is totally ordered. A type that is totally ordered must have all of its members totally
ordered.
This means that if we have a type that defines all the comparison operators but it has a
member that doesn’t then the type is not totally ordered.

With C++20, you can simply =default the spaceship operator and it will generate all the six operators
for you with constexpr and noexcept. These operators will perform a lexicographical comparison.

e.g.
class WrappedInt {
public:
 constexpr WrappedInt(int value): m_value{value} { }
  auto operator<=>(const WrappedInt& rhs) const = default;
 private:
   int m_value;
};
struct NonComparable {
 int a;
};

struct Comparable {
 auto operator<=>(const Comparable& rhs) const = default;
 int a;
};

template <typename T>
void fun(T t) requires std::totally_ordered<T> {
  std::cout << typeid(t).name() << " can be ordered\n";
}

int main() {
 NonComparable nc{666};
 
 // fun(nc);
 // Not OK: error: use of function 'void fun(T) requires totally_ordered<T>
 // [with T = NonComparable]' with unsatisfied constraints
 
 Comparable c{42};
 fun(c);
}

std::copyable for copyable types

template <typename T>
void fun(T t) requires std::copyable<T> {
 std::cout << typeid(t).name() << " is copyable\n";
}


<iterator>

std::indirect_unary_predicate<F, I>

template <typename F, typename I>
void foo(F fun, I iterator) requires std::indirect_unary_predicate<F, I> {
  std::cout << std::boolalpha << fun(*iterator) << '\n';
}

int main() {
  auto biggerThan42 = [](int i){return i > 42;};
  std::vector numbers{15, 43, 66};
  for(auto it = numbers.begin(); it != numbers.end(); ++it) {
    foo(biggerThan42, it);
  }
}


std::indirectly_comparable

template <typename Il, typename Ir, typename F>
void foo(Il leftIterator, Ir rightIterator, F function)
 requires std::indirectly_comparable<Il, Ir, F> {
std::cout << std::boolalpha << function(*leftIterator, *rightIterator) << '\n';
}

int main() {
  using namespace std::string_literals;

  auto binaryLambda = [](int i, int j){ return 42; };
  auto binaryLambda2 = [](int i, std::string j){return 666;};

  std::vector ints{15, 42, 66};
  std::vector floats{15.1, 42.3, 66.6};
  foo(ints.begin(), floats.begin(), binaryLambda);
  // foo(ints.begin(), floats.begin(), binaryLambda2);
  // error: use of function 'void foo(Il, Ir, F) requires
  // indirectly_comparable<Il, Ir, F, std::identity, std::identity>
}

<ranges>

std::ranges::borrowed_range

template <typename R>
void foo(R range) requires std::ranges::borrowed_range<R> {
 std::cout << typeid(range).name() << " is a borrowed range\n";
}

int main() {
  std::vector numbers{15, 43, 66};
  std::string_view stringView{"is this borrowed?"};

// foo(numbers);
// error: use of function 'void foo(R) requires borrowed_range<R>
// [with R = std::vector<int, std::allocator<int> >]' with unsatisfied constraints

  foo(stringView);
}


Use already defined concepts

e.g.
template<typename T>
concept number = std::integral<T> || std::floating_point<T>;


!a is not the opposite of a

What does this mean for us? It means that having something evaluated not to true doesn’t require
that that an expression returns false. It can simply not be compilable at all.

Whereas for a normal boolean expression, we expect that it’s well-formed and each subexpression
is compilable.
That’s the big difference.

For concepts, the opposite of a true expression is not false, but something that is either not wellformed, or false!


e.g.
template <typename T, typename U>
requires std::unsigned_integral<typename T::Blah> ||
  std::unsigned_integral<typename U::Blah>
void foo(T bar, U baz) {
  // ...
}

class MyType {
 public:
 using Blah = unsigned int;
 // ...
};

int main() {
 MyType mt;
 foo(mt, 5);
 foo(5, mt);
// error: no operand of the disjunction is satisfied
// foo(5, 3);
}


Negations come with parentheses

In the requires clause sometimes we wrap everything in between parentheses, sometimes we don’t
have to do so.
It depends on the simplicity of the expression. What is considered simple enough so that no
parentheses are required?
  • bool literals
  • bool variables in any forms among value, value&lt;T>, T::value, trait&lt;T>::value
  • concepts, such as Concept&lt;T>
  • nested requires expressions
  • conjunctions (&&)
  • disjunctions (||)

This list implies that negations cannot be used without parentheses.

i.e.
template <typename T>
 requires (!std::integral<T>) // <--- parentheses needed
T add(T a, T b) {
 return a+b;
}


Subsumption and negations

Read: <Decomposition works as follows> in https://vsdmars.blogspot.com/2022/03/c20-concepts-wrap-up.html

e.g.
template <typename Key>
class Ignition {
public:
 void start(Key key) requires (!smart<Key>) {}

 void start(Key key) requires (!smart<Key>) && personal<Key> {}
};

If we called `Ignition` with a `Key` that is not `smart`, you expect the compiler to subsume that the first
constraints of the two overloads of `Ignition::start()` are common and we have to check whether
the second one applies to our `Key` type or not.

Not the case.

``
main.cpp: In function 'int main()':
main.cpp:25:23: error: call of overloaded 'start(MyKey&)' is ambiguous ignition.start(key);
``

The problem is that () is part of the expression and when it comes to subsumption, the compiler
checks the source location of the expression. Only if two expressions are originating from the same
place, they are considered the same, so the compiler can subsume them.

As () is part of the expression, (!smart<Key>) originates from two different points and those 2 are
not considered the same, they cannot be subsumed. In fact, only with the ! without the enclosing ()
we would face the same problem.

They are considered 2 different constraints, hence the call to start is ambiguous.
That’s why if you need negation and thus you need parentheses, and you rely on subsumption rules,
it’s better to put those negated expressions into their own concepts.

Fix:
template <typename T> concept not_smart = !smart<T>; // <-- same location

template <typename Key>
class Ignition {
 public:
 void start(Key key) requires not_smart<Key> {}

 void start(Key key) requires not_smart<Key> && personal<Key> {}
};


 
template <typename Base, typename Exponent>
concept has_power = std::convertible_to<Exponent, int> &&
 requires (Base base, Exponent exponent) {
  base.power(exponent);
};

class IntWithPower {
 public:
 IntWithPower(int num) : m_num(num) {}
 int power(IntWithPower exp) {
   return pow(m_num, exp);
 }

 operator int() const { return m_num; }

 private:
 int m_num;
};

template<typename Exponent>
void printPower(has_power<Exponent> auto number, Exponent exponent) {
  std::cout << number.power(exponent) << '\n';
}

int main() {
  printPower(IntWithPower{5}, IntWithPower{4});
  printPower(IntWithPower{5}, 4L);
  printPower(IntWithPower{5}, 3.0);
}


Requirements on return types (a.k.a compound requirements)

template <typename T>
concept has_square = requires (T t) {
// take two arguments; https://en.cppreference.com/w/cpp/concepts/convertible_to
  {t.square()} -> std::convertible_to<int>; 
};

{t.square()} -> std::same_as<int>;
{t.square()} -> std::convertible_to<int>;


Require the existence of a nested type

template<typename T>
concept type_requirement = requires {
 typename T::value_type;
};

A class template specialization should name a type In this case, the concept `type_requirement` requires that the class template `Other` can be instantiated with T.
template<typename T>
concept type_requirement = requires {
 typename Other<T>;
};

template <typename T>
  requires (!std::same_as<T, std::vector<int>>) // parentheses are mandatory!
struct Other{};

Thus, the line type_requirement
`auto myVec = std::vector&lt;int>{1, 2, 3}; fails;`

template <typename T>
  requires (!std::same_as<T, std::vector<int>>)
struct Other{};

template<typename T>
concept type_requirement = requires {
  typename Other<T>;
};

int main() {
  type_requirement auto myVec = std::vector<int>{1, 2, 3};
}


An alias template specialization should name a type

template<typename T> using ValueTypeOf = T::value_type;

template<typename T>
concept type_requirement = requires {
 typename ValueTypeOf<T>;
};

int main() {
  type_requirement auto myVec = std::vector<int>{1, 2, 3};
}
Nested requirements
template<typename C>
concept clonable = requires (C clonable) {
  { clonable.clone() } -> std::same_as<C>;
  requires std::same_as<C*, decltype(&clonable)>;
};


The need for multiple destructors

Why would you need multiple destructors?
For optimization reasons.

before C++20; use std::conditional with different base types.

e.g.
class Wrapper_Trivial {
public:
 ~Wrapper_Trivial() = default;
};

class Wrapper_NonTrivial {
public:
 ~Wrapper_NonTrivial() {
  std::cout << "Not trivial\n";
 }
};

template <typename T>
class Wrapper : public std::conditional_t<
 std::is_trivially_destructible_v<T>,
   Wrapper_Trivial, Wrapper_NonTrivial> {
 T t;
};

int main() {
  Wrapper<int> wrappedInt;
  Wrapper<std::string> wrappedString;
}

with C++20
e.g.
template <typename T>
class Wrapper {
 T t;
public:
 ~Wrapper() = default; // should defined first, otherwise clang complains.

 ~Wrapper() requires (!std::is_trivially_destructible_v<T>) {
   std::cout << "Not trivial\n";
 }
};

int main() {
  Wrapper<int> wrappedInt;
  Wrapper<std::string> wrappedString;
}

No comments:

Post a Comment

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