Steal this Class
Class design in C++ is hard. Really, really, hard. I love C++ but, sadly, all the defaults are wrong.
This means you have to add a lot of “ornamentation” to a well-designed C++ class. This can seem bulky, so you might start second-guessing and questioning yourself, and it’s easy to forget a thing or two.
To make things worse, much of the advice on Stack Overflow and (LLVM output trained on that advice) is outdated, incomplete, and just wrong.
Since Ctrl-C, Ctrl-V is the de facto standard programming paradigm, and you are going to do it regardless, consider using these boilerplate classes. They are “copy-paste safe.” They have the right defaults, and they are not missing some essential piece that will get you into trouble later.
Of course, we have the C++ Core Guidelines. These are all the defaults and best practices you should be using. But, be honest, you’re not going to read those, are you? These are the templates you have been looking for.
A Plain POD
struct Point {
// A1 Use 'struct' for these, and only these, basic POD (Plain Old Data) types.
// This reduces clutter by providing public visibility by default.
constexpr bool operator==(const Point&) const noexcept = default;
// A2 Add the == operator (!= added implicitly) to make the type "regular"
// A3 Make the operators free functions in C++14 & 17
// A4 Provide explicit operator!= in C++14 & 17
float x{};
float y{};
// A5 Add {} to members of primitive type to make sure they are initialized.
// A6 Keep members after the functions. In case you need to convert it to a
// class, this will produce a smaller git diff (less risk for conflicts).
// A7 Order members by "type with largest alignment" first, to keep the
// struct small. Doubles first, then ints, then chars.
};
Basic Data Types with Invariants
class Circle {
// B1 Make anything with logic a class (not a struct)
public:
constexpr Circle() = default;
// B2 Always make basic data types default constructible to be "regular"
// B3 constexpr makes the data type more useful
constexpr explicit Circle(float radius) : Circle(Point{}, radius) {}
// B4 Use 'explicit' for all single argument constructors to prevent
// unintentional implicit conversion.
// B5 Limit duplication in constructor bodies with delegating constructors
constexpr Circle(const Point & center, float radius) :
center_(center), radius_(radius < 0.0f ? 0.0f : radius) {}
// B6 Handle pre-condition violations and preserve class invariants
// by truncation or throwing. Throw unless constructor is constexpr.
// B7 NO: constexpr Circle(float x, float y, float radius)
// Don't offer this constructor to avoid confusion with argument order
// at the call site, i.e. Circle{radius, x, y}
[[nodiscard]] constexpr const Point & getCenter() const noexcept { return center_; }
// B8 Use both constexpr and const
// B9 Use [[nodiscard]] where it's clearly a bug to not use a return value
// B10 All logically non-mutating functions should be const
constexpr void setCenter(const Point & center) { center_ = center; }
// B11 Setters can often not be noexcept since they might copy and allocate
// In this case it would be fine, but not in general.
[[nodiscard]] constexpr float getRadius() const noexcept { return radius_; }
constexpr void setRadius(float radius) { radius_ = radius < 0.0f ? 0.0f : radius; }
// B12 Preserve invariants in setters (or throw)
// B13 Take arguments as value in case they're small enough to be passed
// in a register, i.e. all built-in types and smart pointers.
constexpr bool operator==(const Circle&) const noexcept = default;
// B14 Add the == operator (!= added implicitly) to make the type "regular"
// B15 Make the operators free functions in C++14 & 17
// B16 Provide explicit operator!= in C++14 & 17
private:
// B17 All mutable members are non const, non ref and private
Point center_{};
float radius_{};
// B18 Use trailig underscore to keep good parameter names in setter
// function signatures.
};
Resource Owning RAII Types
class File {
// C1 RAII types should be classes (not structs)
public:
[[nodiscard]] explicit File(const std::filesystem::path& fileName)
: file_(std::fopen(fileName.c_str(), "r")) {
if (!file_) throw std::runtime_error("Failed to open file");
}
// C2 [[nodiscard]] on constructors prevents forgetting to name the RAII.
// Especially important if the 'resource' is a side-effect, like a lock.
// C3 Use exceptions to signal errors
// C4 Make single-arg constructor 'explicit' to prevent implicit conversion.
~File() noexcept {
if (file_) std::fclose(file_);
}
// C5 All destructors should be noexcept
// C6 Not 'virtual' since it is not meant as a base class
File(File&& other) noexcept : file_(std::exchange(other.file_, nullptr)) {}
// C7 Use std::exchange to leave the moved-from object in a clean state
File& operator=(File other) noexcept {
std::swap(file_, other.file_);
return *this;
}
// C8 Take arg by value. Parameter will be created by move ctor, not copy.
// C9 Use std::swap to avoid leaking the assigned to resource
// C10 Only check for self-assignment if members are not self assignable
// C11 Make move operations noexcept to work well with std::vector, etc.
File(const File&) = delete;
File& operator=(const File&) = delete;
// C12 RAII types should typically not be copyable. Be explicit (rule of 0/5).
private:
FILE* file_ = nullptr;
// C13 Initialize to nullptr in case there's an exception before
// initializer-list assignment.
};
Polymorphic Types
class IPolygon {
// D1 Interfaces should be classes (not structs)
// D2 I-prefix makes "Polygon" name available for subclass
public:
virtual ~IPolygon() noexcept = default;
// D3 Add a virtual destructor to ensure derived class destructor is called
// D4 All destructors should be noexcept
[[nodiscard]] virtual double sum_of_angles() const noexcept = 0;
// D5 Pure virtual interface API functions
// D6 noexcept enforces noexcept in subclasses (use if desirable)
// D7 [[nodiscard]] and const for getters
protected:
IPolygon() = default;
// D8 protected constructor to prevent instantiation of interface class
// D9 Not private to make default constructor available in subclass
private:
IPolygon(const IPolygon&) = delete;
IPolygon& operator=(const IPolygon&) = delete;
IPolygon(IPolygon&&) = delete;
IPolygon& operator=(IPolygon&&) = delete;
// D10 remove value semantics for polymorphic class hierarchies
};
class Triangle final : public IPolygon {
// D11 Inheritance should be public
// D12 final prevents unintended extension of hierarchy
public:
Triangle() = default;
// D13 Default constructor here finds default constructor in base
virtual ~Triangle() noexcept = default;
// D14 virtual even if it's not neccessary since class is final
// If final is removed, this prevents a hard to find bug.
[[nodiscard]] double sum_of_angles() const noexcept override { return 180.0; }
// D15 Use override. virtual is implied and not required
// D16 noexcept required by base class
// NO: Triangle(const Triangle&) = delete;
// D17 No need to remove value semantics here since it's done in base
};
• • •
Now, before you get too worked up and start yelling at the screen. I know, there are exceptions to almost all of the recommendations in this post. But they are exceptions. What I have presented are the safe and sane defaults. Your starting point for modern C++.
• • •