C++ Core Guidelines: Ordering of User-Defined Types

My post for today is just loosely coupled to the rules of the C++ core guidelines because they provide not much content. Inspired by the Guidelines, today's post is about a generic isSmaller function.

hammer 802301 1280 

Here are the rules for today. They are about the specialisation of templates.

The specialisation of function templates is also one of my topics for today.

Comparing two Accounts: The First

Let me start simple. I have a class Account. I want to know for an account if it is smaller than another account. Smaller means in this case that the balance is lower.

// isSmaller.cpp

#include <iostream>

class Account{
public:
  Account() = default;
  Account(double b): balance(b){}
  double getBalance() const {
    return balance;
  }
private:
  double balance{0.0};
};

template<typename T>                     // (1)
bool isSmaller(T fir, T sec){
    return fir < sec;
}

int main(){

    std::cout << std::boolalpha << std::endl;

    double firDoub{};
    double secDoub{2014.0};

    std::cout << "isSmaller(firDoub, secDoub): " 
              << isSmaller(firDoub, secDoub) << std::endl;

    Account firAcc{};
    Account secAcc{2014.0};

    std::cout << "isSmaller(firAcc, secAcc): " 
              << isSmaller(firAcc, secAcc) << std::endl;
    
    std::cout << std::endl;

}

To make my job easy, I wrote a generic isSmaller function (1) for comparing two values. As you presumably expected, I can not compare accounts because it's operator< is not overloaded.

 isSmaller

Before I variously solve this issue, I want to make a short detour to SemiRegular and Regular types. This for one reason because the original definition of Regular from Alexander Stepanov and the definition as a concept in C++20 differs in one critical point: ordering.

SemiRegular and Regular Types

The rule T.67: Use specialization to provide alternative implementations for irregular types talks about irregular types. The informal term irregular types stand for types which are either SemiRegular nor Regular. Just to remind you. Here is the definition of SemiRegular and Regular types.

Regular

  • DefaultConstructible
  • CopyConstructible, CopyAssignable
  • MoveConstructible, MoveAssignable
  • Destructible
  • Swappable
  • EqualityComparable

SemiRegular

  • SemiRegular - EqualityComparable

If you want to know more details to Regular and SemiRegular, read my post  C++ Core Guidelines: Regular and SemiRegular Types. Account is a SemiRegular but not a Regular type.

 

// accountSemiRegular.cpp

#include <experimental/type_traits>
#include <iostream>

class Account{
public:
  Account() = default;
  Account(double b): balance(b){}
  double getAccount() const {
    return balance;
  }
private:
  double balance{0.0};
};

template<typename T>
using equal_comparable_t = decltype(std::declval<T&>() == std::declval<T&>());

template<typename T>
struct isEqualityComparable: 
       std::experimental::is_detected<equal_comparable_t, T>
       {};

template<typename T>
struct isSemiRegular: std::integral_constant<bool,
                                      std::is_default_constructible<T>::value &&
                                      std::is_copy_constructible<T>::value &&
                                      std::is_copy_assignable<T>::value &&
                                      std::is_move_constructible<T>::value &&
                                      std::is_move_assignable<T>::value &&
                                      std::is_destructible<T>::value &&
                                      std::is_swappable<T>::value >{};

template<typename T>
struct isRegular: std::integral_constant<bool, 
                                         isSemiRegular<T>::value &&
                                         isEqualityComparable<T>::value >{};

int main(){

    std::cout << std::boolalpha << std::endl;

    std::cout << "isSemiRegular<Account>::value: " << isSemiRegular<Account>::value << std::endl;
    std::cout << "isRegular<Account>::value: " << isRegular<Account>::value << std::endl;

    std::cout << std::endl;

}

 

The output of the program shows it.

 accountSemiRegular

For the details to the program, read my already mentioned post: C++ Core Guidelines: Regular and SemiRegular Types.

By adding the operator == to Account, Account becomes Regular.

// accountRegular.cpp

#include <iostream>

class Account{
public:
    Account() = default;
    Account(double b): balance(b){}
    friend bool operator == (Account const& fir, Account const& sec) {   // (1)
        return fir.getBalance() == sec.getBalance();
    }
    double getBalance() const {
        return balance;
    }
private:
    double balance{0.0};
};

template<typename T>
bool isSmaller(T fir, T sec){
    return fir < sec;
}

int main(){

    std::cout << std::boolalpha << std::endl;
    
    double firDou{};
    double secDou{2014.0};

    std::cout << "isSmaller(firDou, secDou): " << isSmaller(firDou, secDou) << std::endl;

    Account firAcc{};
    Account secAcc{2014.0};

    std::cout << "isSmaller(firAcc, secAcc): " << isSmaller(firAcc, secAcc) << std::endl;
    
    std::cout << std::endl;

}

 

But Account is still not comparable.

accountRegular

This is the crucial difference between Regular types defined by Alexander Stepanov and the concept Regular which we get with C++20. Due to Alexander Stepanov, a Regular type should support a  total ordering.

Now, I come back to my original plan.

Comparing Two Accounts: The Second

The key idea for my variations is that instances of Account should support the generic isSmaller function.

Overloading operator <

Overloading operator < is the probably the most obvious way. Even the error message to the program isSmaller.cpp showed it.

// accountIsSmaller1.cpp

#include <iostream>

class Account{
public:
    Account() = default;
    Account(double b): balance(b){}
    friend bool operator == (Account const& fir, Account const& sec) { 
        return fir.getBalance() == sec.getBalance();
    }
    friend bool operator < (Account const& fir, Account const& sec) { 
        return fir.getBalance() < sec.getBalance();
    }
    double getBalance() const {
      return balance;
    }
private:
    double balance{0.0};
};

template<typename T>
bool isSmaller(T fir, T sec){
    return fir < sec;
}

int main(){

    std::cout << std::boolalpha << std::endl;
    
    double firDou{};
    double secDou{2014.0};

    std::cout << "isSmaller(firDou, secDou): " 
              << isSmaller(firDou, secDou) << std::endl;

    Account firAcc{};
    Account secAcc{2014.0};

    std::cout << "isSmaller(firAcc, secAcc): " 
              << isSmaller(firAcc, secAcc) << std::endl;
    
    std::cout << std::endl;

}

 

The output of this and the next program is the same; therefore, I skip it.

Full Specialisation of isSmaller

If you can not change the definition of Account, you can at least fully specialise isSmaller for Account.

// accountIsSmaller2.cpp

#include <iostream>

class Account{
public:
    Account() = default;
    Account(double b): balance(b){}
    friend bool operator == (Account const& fir, Account const& sec) { 
        return fir.getBalance() == sec.getBalance();
    }
    double getBalance() const {
      return balance;
    }
private:
    double balance{0.0};
};

template<typename T>
bool isSmaller(T fir, T sec){
    return fir < sec;
}

template<>
bool isSmaller<Account>(Account fir, Account sec){
    return fir.getBalance() < sec.getBalance();
}

int main(){

    std::cout << std::boolalpha << std::endl;
    
    double firDou{};
    double secDou{2014.0};

    std::cout << "isSmaller(firDou, secDou): " 
              << isSmaller(firDou, secDou) << std::endl;

    Account firAcc{};
    Account secAcc{2014.0};

    std::cout << "isSmaller(firAcc, secAcc): " 
              << isSmaller(firAcc, secAcc) << std::endl;
    
    std::cout << std::endl;

}

 

By the way, a non-generic function bool isSmaller(Account fir, Account sec) would also do the job.

Extend isSmaller with a Binary Predicate

There is another way to extend isSmaller. I spend the generic function an additional type parameter Pred which should hold the binary predicate. This pattern is often used in the standard template library.

 

// accountIsSmaller3.cpp

#include <functional>
#include <iostream>
#include <string>

class Account{
public:
    Account() = default;
    Account(double b): balance(b){}
    friend bool operator == (Account const& fir, Account const& sec) { 
        return fir.getBalance() == sec.getBalance();
    }
    double getBalance() const {
      return balance;
    }
private:
    double balance{0.0};
};

template <typename T, typename Pred = std::less<T> >    // (1)
bool isSmaller(T fir, T sec, Pred pred = Pred() ){      // (2)
  return pred(fir, sec);                                // (3)
}

int main(){

    std::cout << std::boolalpha << std::endl;
    
    double firDou{};
    double secDou{2014.0};

    std::cout << "isSmaller(firDou, secDou): " 
              << isSmaller(firDou, secDou) << std::endl;

    Account firAcc{};
    Account secAcc{2014.0};
    
    auto res = isSmaller(firAcc, secAcc,                               // (4)
                         [](const Account& fir, const Account& sec){
                             return fir.getBalance() < sec.getBalance(); 
                         }
    );
    
    std::cout << "isSmaller(firAcc, secAcc): " <<  res << std::endl;
    
    std::cout << std::endl;
    
    std::string firStr = "AAA";
    std::string secStr = "BB";
     
    std::cout << "isSmaller(firStr, secStr): " 
              <<  isSmaller(firStr, secStr) << std::endl;
    
     auto res2 = isSmaller(firStr, secStr,                             // (5)
                           [](const std::string& fir, const std::string& sec){
                               return fir.length() < sec.length(); 
                           }
    );
     
    std::cout << "isSmaller(firStr, secStr): " <<  res2 << std::endl;
    
    std::cout << std::endl;

}

The generic function has std::less<T> as the default ordering (1). The binary predicate Pred is instantiated in line (2) and used in line (3). If you don't specify the binary predicate, std::less is used. Additionally, you can provide your binary predicate such as in line (4) or line (5). A lambda function is an ideal fit for this job.

Finally, here is the output of the program:

accountIsSmaller3

What are the differences between this three techniques?

Comparing two Accounts: The Third

ComparisonNew

The full specialisation is not a general solution because it only works for the function isSmaller. In contrast, the operator < is quite often applicable, and the predicate can be used by any type. The operator < and the full specialisation are static. This means, the ordering is defined at compile time and is encoded in the type or the generic function. In contrast, the extension can be invoked with different predicates. This is a runtime decision. The operator < extends the type, the other both variants the function. The extension with a predicate allows it to order your type in various ways. For example, you can compare strings lexicographically or by their length.

Based on this comparison a good rule of thumb is it to implement an operator < for your types and add an extension to your generic functions if necessary. Therefore, your type behaves Regular according to Alexander Stepanov and also supports customised orderings.

What's next?

The next post is about templates. In particular, it is about template hierarchies.

 

 

 

Thanks a lot to my Patreon Supporters: Eric Pederson, Paul Baxter,  Meeting C++, Matt Braun, Avi Lachmish, Roman Postanciuc, Venkata Ramesh Gudpati, Tobias Zindl, Dilettant, Marko, Ramesh Jangama, and Emyr Williams.

 

Thanks in particular to:  TakeUpCode 450 60

 

Get your e-book at Leanpub:

The C++ Standard Library

 

Concurrency With Modern C++

 

Get Both as one Bundle

cover   ConcurrencyCoverFrame   bundle
With C++11, C++14, and C++17 we got a lot of new C++ libraries. In addition, the existing ones are greatly improved. The key idea of my book is to give you the necessary information to the current C++ libraries in about 200 pages.  

C++11 is the first C++ standard that deals with concurrency. The story goes on with C++17 and will continue with C++20.

I'll give you a detailed insight in the current and the upcoming concurrency in C++. This insight includes the theory and a lot of practice with more the 100 source files.

 

Get my books "The C++ Standard Library" (including C++17) and "Concurrency with Modern C++" in a bundle.

In sum, you get more than 600 pages full of modern C++ and more than 100 source files presenting concurrency in practice.

 

Get your interactive course

 

Modern C++ Concurrency in Practice

C++ Standard Library including C++14 & C++17

educative CLibrary

Based on my book "Concurrency with Modern C++" educative.io created an interactive course.

What's Inside?

  • 140 lessons
  • 110 code playgrounds => Runs in the browser
  • 78 code snippets
  • 55 illustrations

Based on my book "The C++ Standard Library" educative.io created an interactive course.

What's Inside?

  • 149 lessons
  • 111 code playgrounds => Runs in the browser
  • 164 code snippets
  • 25 illustrations
Tags: templates

Add comment


Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 2598

All 1711857

Currently are 131 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments