C++ Core Guidelines: Definition of Concepts, the Second

C++ Core Guidelines: Definition of Concepts, the Second

This is a cross-post from www.ModernesCpp.com.

Let me continue with the rules for defining concepts in the guidelines. In this post, the first of the three remaining rules is quite sophisticated.

 Here are the rules for today:

The explanation to the first rules is quite concise. Maybe, too concise.

T.24: Use tag classes or traits to differentiate concepts that differ only in semantics

This is the reason for this rule from the guidelines: "Two concepts requiring the same syntax but having different semantics leads to ambiguity unless the programmer differentiates them."

Let's assume; I defined the is_contiguous trait. In this case, I can use it to distinguish a random access iterator RA_iter from a contiguous iterator Contiguous_iter.

template<typename I>    // iterator providing random access
concept bool RA_iter = ...;

template<typename I>    // iterator providing random access to contiguous data
concept bool Contiguous_iter =
    RA_iter<I> && is_contiguous<I>::value;  // using is_contiguous trait

 I can even wrap a tag class such as is_contiguous into a concept an use it. Now, I have a more straightforward expression of my idea contiguous iterator Contiguous_iter.

template<typename I> concept Contiguous = is_contiguous<I>::value;

template<typename I>
concept bool Contiguous_iter = RA_iter<I> && Contiguous<I>;

 Okay, let me first explain two key terms: traits and tag dispatching.

Traits

Traits are class templates which extract properties from a generic type. 

The following program presents for each of the 14 primary type categories of the type-traits library a type which satisfies the specific trait. The primary type categories are complete and don’t overlap. So each type is a member of a type category. If you check a type category for your type, the request is independent of the const or volatile qualifiers.

// traitsPrimary.cpp

#include <iostream>
#include <type_traits>

using namespace std;

template <typename T>
void getPrimaryTypeCategory(){

  cout << boolalpha << endl;

  cout << "is_void<T>::value: " << is_void<T>::value << endl;
  cout << "is_integral<T>::value: " << is_integral<T>::value << endl;
  cout << "is_floating_point<T>::value: " << is_floating_point<T>::value << endl;
  cout << "is_array<T>::value: " << is_array<T>::value << endl;
  cout << "is_pointer<T>::value: " << is_pointer<T>::value << endl;
  cout << "is_null_pointer<T>::value: " << is_null_pointer<T>::value << endl;
  cout << "is_member_object_pointer<T>::value: " << is_member_object_pointer<T>::value << endl;
  cout << "is_member_function_pointer<T>::value: " << is_member_function_pointer<T>::value << endl;
  cout << "is_enum<T>::value: " << is_enum<T>::value << endl;
  cout << "is_union<T>::value: " << is_union<T>::value << endl;
  cout << "is_class<T>::value: " << is_class<T>::value << endl;
  cout << "is_function<T>::value: " << is_function<T>::value << endl;
  cout << "is_lvalue_reference<T>::value: " << is_lvalue_reference<T>::value << endl;
  cout << "is_rvalue_reference<T>::value: " << is_rvalue_reference<T>::value << endl;

  cout << endl;

}

int main(){
    
    getPrimaryTypeCategory<void>();              // (1)
    getPrimaryTypeCategory<short>();             // (1)
    getPrimaryTypeCategory<double>();
    getPrimaryTypeCategory<int []>();
    getPrimaryTypeCategory<int*>();
    getPrimaryTypeCategory<std::nullptr_t>();
    struct A{
        int a;
        int f(double){return 2011;}
    };
    getPrimaryTypeCategory<int A::*>();
    getPrimaryTypeCategory<int (A::*)(double)>();
    enum E{
        e= 1,
    };
    getPrimaryTypeCategory<E>();
    union U{
      int u;
    };
    getPrimaryTypeCategory<U>();
    getPrimaryTypeCategory<string>();
    getPrimaryTypeCategory<int * (double)>();
    getPrimaryTypeCategory<int&>();              // (2)         
    getPrimaryTypeCategory<int&&>();             // (2)
    
}

 I don't want to bore you to death. Therefore, there is only the output of the lines (1).

And here is the output of the lines (2).

Tag Dispatching

Tag dispatching enables it to choose a function based on the properties of its types. The decision takes place at compile time and traits which I explained the last paragraph are used. 

A typical example of tag dispatching is the std::advance algorithm from the Standard Template Library. std::advance(it, n) increments the iterator it by n elements. The program shows you the key idea.

 // advanceTagDispatch.cpp
#include <iterator>
#include <forward_list>
#include <list>
#include <vector>
#include <iostream>

template <typename InputIterator, typename Distance>
void advance_impl(InputIterator& i, Distance n, std::input_iterator_tag) {
	std::cout << "InputIterator used" << std::endl; 
    while (n--) ++i;
}

template <typename BidirectionalIterator, typename Distance>
void advance_impl(BidirectionalIterator& i, Distance n, std::bidirectional_iterator_tag) {
	std::cout << "BidirectionalIterator used" << std::endl;
    if (n >= 0) 
        while (n--) ++i;
    else 
        while (n++) --i;
}

template <typename RandomAccessIterator, typename Distance>
void advance_impl(RandomAccessIterator& i, Distance n, std::random_access_iterator_tag) {
	std::cout << "RandomAccessIterator used" << std::endl;
    i += n;
}

template <typename InputIterator, typename Distance>
void advance_(InputIterator& i, Distance n) {
    typename std::iterator_traits<InputIterator>::iterator_category category;    // (1)
    advance_impl(i, n, category);                                                // (2)
}
  
int main(){
    
    std::cout << std::endl;
    
    std::vector<int> myVec{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    auto myVecIt = myVec.begin();                                                // (3)std::cout << "*myVecIt: " << *myVecIt << std::endl;
    advance_(myVecIt, 5);
    std::cout << "*myVecIt: " << *myVecIt << std::endl;
    
    std::cout << std::endl;
    
    std::list<int> myList{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    auto myListIt = myList.begin();                                              // (4)std::cout << "*myListIt: " << *myListIt << std::endl;
    advance_(myListIt, 5);
    std::cout << "*myListIt: " << *myListIt << std::endl;
    
    std::cout << std::endl;
    
    std::forward_list<int> myForwardList{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    auto myForwardListIt = myForwardList.begin();                                // (5)std::cout << "*myForwardListIt: " << *myForwardListIt << std::endl;
    advance_(myForwardListIt, 5);
    std::cout << "*myForwardListIt: " << *myForwardListIt << std::endl;
    
    std::cout << std::endl;
    
}

The expression std::iterator_traits::iterator_category category determines the iterator category at compile time. Based on the iterator category the most specific variable of the function advance_impl(i, n, category) is used in line (2). Each container returns an iterator of the iterator category which corresponds to its structure. Therefore, line (3) gives a random access iterator, line (4) gives a bidirectional iterator, and line (5) gives a forward iterator which is also an input iterator.

From the performance point of view, this distinction makes a lot of sense because a random access iterator can be faster incremented than a bidirectional iterator, and a bidirectional iterator can be faster incremented than an input iterator. From the users perspective, you invoke std::advance(it, 5) and you get the fastest version which your container satisfies.

This was quite verbose. I have not so much to add the two remaining rules.

T.25: Avoid complimentary constraints

The example from the guidelines shows complimentary constraints.

template<typename T> 
    requires !C<T> // bad 
void f(); 

template<typename T> 
    requires C<T> 
void f();

Avoid it. Make an unconstrained template and a constrained template instead.

 template<typename T>   // general template
    void f();

template<typename T>   // specialization by concept
    requires C<T>
void f();

 You can even set the unconstrained version to delete such that the constrained versions is only usable.

template<typename T>
void f() = delete;

T.26: Prefer to define concepts in terms of use-patterns rather than simple syntax

The title for this guideline is quite vague, but the example is self-explanatory.

Instead of using the concepts has_equal and has_not_equal to define the concept Equality

template<typename T> concept Equality = has_equal<T> && has_not_equal<T>;

use the usage-pattern. This is more readable than the previous version:

template<typename T> concept Equality = requires(T a, T b) {
    bool == { a == b }
    bool == { a != b }
    // axiom { !(a == b) == (a != b) }// axiom { a = b; => a == b }  // => means "implies"

The concept Equality requires in this case that you can apply == and != to the arguments and both operations return bool.

What's next?

Here is a part of the opening from the C++ core guidelines to template interfaces: "...the interface to a template is a critical concept - a contract between a user and an implementer - and should be carefully designed.". You see, the next post is critical.

Rainer Grimm

Trainer at Modernes C++

6 年

Good idea: https://www.modernescpp.com/index.php/c-core-guidelines-rules-for-the-definition-of-concepts. You can always use the link to my page at the beginning of the post. On my blog www.ModernesCpp.com is an overview page (Start Here) because I have written about 250 posts in the last years.

回复
Aymen CHEHAIDER

Ingénieur logiciels sénior chez Total

6 年

Nice Article. I would add a little suggestion here, as I didn't see the first post of concepts, may be adding a link to the related article would be great.??

回复

要查看或添加评论,请登录

Rainer Grimm的更多文章

  • std::execution: Asynchronous Algorithms

    std::execution: Asynchronous Algorithms

    supports many asynchronous algorithms for various workflows. Presenting proposal P2300R10 is not easy.

  • My ALS Journey (17/n): Christmas Special

    My ALS Journey (17/n): Christmas Special

    Today, I have a special Christmas gift. My ALS Journey so far Make the Difference Let’s do something great together:…

  • std::execution

    std::execution

    std::execution, previously known as executors or Senders/Receivers, provides “a Standard C++ framework for managing…

    1 条评论
  • C++26 Core Language: Small Improvements

    C++26 Core Language: Small Improvements

    There are more small improvements in the C++26 language, which you should know. static_assert extension First, here’s…

  • My ALS Journey (16/n): Good Bye Training / Hello Mentoring

    My ALS Journey (16/n): Good Bye Training / Hello Mentoring

    In 2025, I will no longer offer C++ classes. Instead, I will only offer C++ mentoring in the future.

    1 条评论
  • Placeholders and Extended Character Set

    Placeholders and Extended Character Set

    Placeholders are a nice way to highlight variables that are no longer needed. Additionally, the character set of C++26…

    4 条评论
  • Contracts in C++26

    Contracts in C++26

    Contracts allow you to specify preconditions, postconditions, and invariants for functions. Contracts should already be…

    5 条评论
  • Mentoring as a Key to Success

    Mentoring as a Key to Success

    I know that we are going through very challenging times. Saving is the top priority.

  • Reflection in C++26: Determine the Layout

    Reflection in C++26: Determine the Layout

    Thanks to reflection, you can determine the layout of types. My examples are based on the reflection proposal P2996R5.

    5 条评论
  • My ALS Journey (15/n): A typical Day

    My ALS Journey (15/n): A typical Day

    You may wonder how my day looks. Let me compare a day before ALS with a current day.

    3 条评论

社区洞察

其他会员也浏览了