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
thus we could explicit specialize it to enable 'view'' trait.
e.g.
This is due to in std we have enable_view defined as:
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;
};
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
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:
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.
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
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.
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<T>, T::value, trait<T>::value
- concepts, such as Concept<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> {}
};
CppCoreGuidelines
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{};
`auto myVec = std::vector<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.