Observer

C++ Core Guidelines: Class Rules

A class is a user-defined type where the programmer can specify the representation, the operations, and the interface. The C++ core guidelines have a lot of rules for user-defined types.

The guidelines start with general rules but also include special rules for constructors and destructors, class hierarchies, overloading of operators, and unions.

Before I write about the special rules that are way more interesting, here are the eight general rules.

I will only write as much to the general class rules to make their intention clear.

General rules for classes

Observer

C.1: Organize related data into structures (structs or classes)

If data is related, you should put it into a struct or class; therefore, the second function is straightforward to comprehend.

void draw(int x, int y, int x2, int y2);  // BAD: unnecessary implicit relationships
void draw(Point from, Point to);          // better

 

C.2: Use class if the class has an invariant; use struct if the data members can vary independently

An invariant is a logical condition that a constructor typically establishes.

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (open)
  • "Generic Programming (Templates) with C++": October 2024
  • "Embedded Programming with Modern C++": October 2024
  • "Clean Code: Best Practices for Modern C++": March 2025
  • Do you want to stay informed: Subscribe.

     

    struct Pair {  // the members can vary independently
        string name;
        int volume;
    };
    
    class Date {
    public:
        // validate that {yy, mm, dd} is a valid date and initialize
        Date(int yy, Month mm, char dd);
        // ...
    private:
        int y;
        Month m;
        char d;    // day
    };
    

     

    The class Date has the invariants y, m, and d. They are initialized and checked in the constructor. The data type Pair has no invariant; therefore, it is a struct.

    Due to the invariance, the class is easier to use. This is precisely the aim of the following rule.

    C.3: Represent the distinction between an interface and an implementation using a class

    The public methods are, in this case, the interface of a class, and the private part is the implementation.

     

    class Date {
        // ... some representation ...
    public:
        Date();
        // validate that {yy, mm, dd} is a valid date and initialize
        Date(int yy, Month mm, char dd);
    
        int day() const;
        Month month() const;
        // ...
    };
    

     

    From a maintainability perspective, the implementations of the class Date can be changed without affecting the user.

    C.4: Make a function a member-only if it needs direct access to the representation of a class

    If a function needs no access to the internals of the class, it should not be a member. Hence you get loose coupling, and a change in the internals of the class will not affect the function.

    C.5: Place helper functions in the same namespace as the class they support

    Such a helper function should be in the namespace of the class.

    namespace Chrono { // here we keep time-related services
    
        class Date { /* ... */ };
    
        // helper functions:
        bool operator==(Date, Date);
        Date next_weekday(Date);
        // ...
    }
    ...
    if (date1 == date2){ ... // (1)

     

    Thanks to argument-dependent lookup (ADL), the comparison in (1) will additionally look for the identity operator in the Chrono namespace.

    C.7: Don’t define a class or enum and declare a variable of its type in the same statement

    I admit: defining a class and declaring a variable of its type in the same statement confuses me.

     

    // bad
    struct Data { /*...*/ } data{ /*...*/ }; 
    
    // good
    struct Data { /*...*/ };         
    Data data{ /*...*/ };
    

    C.8: Use class rather than struct if any member is non-public

    This is quite a valid and often-used convention. If a data type has private or protected members, make it a class.

    C.9: Minimize exposure of members

    This rule, also called data hiding, is one of the cornerstones of object-oriented class design. It means that you should think about two interfaces for your class. A public interface for the general use case and a protected interface for derived classes. The remaining members should be private.

    I will continue with the more special rules. Here is an overview:

    Let’s continue with the two rules to concrete types.

    Concrete types

    First of all, I have to write about concrete types and regular types.

    A concrete type is “the simplest kind of a class”. It is often called a value type and is not part of a type hierarchy. Of course, an abstract type can not be concrete.

    A regular type is a type that “behaves like an int” and has, therefore, to support copy and assignment, equality, and order. To be more formal. A regular type Regular supports the following operations.

    •  Copy and assignment
    Regular a;
    Regular a  = b;
    ~Regular(a);
    a = b;
    
    • Equality

    a == b;
    a != b;
    • Ordering

    a < b;

     

    The built-in types are regular such as the container of the standard template library.

    C.10: Prefer concrete types over class hierarchies

    Use a concrete type if you have no use case for a class hierarchy. A concrete type is way easier to implement, smaller, and faster. You do have not to think about inheritance, virtuality, references, or pointers, including memory allocation and deallocation. There will be no virtual dispatch and, therefore, no runtime overhead.

    You have value.

    C.11: Make concrete types regular

    Regular types (ints) are easier to understand. They are, per see, intuitive. If you have a concrete type think about upgrading it to a regular type.

    What’s next

    The next post will be about the lifetime of objects: create, copy, move, and destroy.

     

     

     

    Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery,and Matt Godbolt.

    Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.

    My special thanks to Embarcadero
    My special thanks to PVS-Studio
    My special thanks to Tipi.build 
    My special thanks to Take Up Code
    My special thanks to SHAVEDYAKS

    Modernes C++ GmbH

    Modernes C++ Mentoring (English)

    Do you want to stay informed about my mentoring programs? Subscribe Here

    Rainer Grimm
    Yalovastraße 20
    72108 Rottenburg

    Mobil: +49 176 5506 5086
    Mail: schulung@ModernesCpp.de
    Mentoring: www.ModernesCpp.org

    Modernes C++ Mentoring,

     

     

    0 replies

    Leave a Reply

    Want to join the discussion?
    Feel free to contribute!

    Leave a Reply

    Your email address will not be published. Required fields are marked *