Released · improving
C++ guide · 6/6
Four ideas separate idiomatic modern C++ from "C with classes": RAII, smart pointers, move semantics and templates.
RAII (Resource Acquisition Is Initialization) means an object acquires a resource in its constructor and releases it in its destructor. A local object's destructor always runs when it leaves scope, even if an exception passes through, so resources owned this way cannot leak.
#include <cstdio>
#include <stdexcept>
class File {
public:
File(const char* path, const char* mode) : handle_{std::fopen(path, mode)} {
if (!handle_) throw std::runtime_error("could not open file");
}
~File() { std::fclose(handle_); } // closed automatically at end of scope
File(const File&) = delete; // no copies: never close the same handle twice
File& operator=(const File&) = delete;
std::FILE* get() const noexcept { return handle_; }
private:
std::FILE* handle_;
};
void write_log() {
File f{"log.txt", "w"};
std::fputs("start\n", f.get());
// even if something below throws, f's destructor closes the file
}The standard library works the same way: std::vector and std::string own memory, std::ifstream owns a file, std::lock_guard owns a mutex lock. Modern code rarely needs a bare new or delete.
Heap objects should be owned by a smart pointer from <memory>.
| Type | Ownership | How to create |
|---|---|---|
std::unique_ptr<T> | Sole owner. Movable, not copyable | std::make_unique<T>(...) (C++14) |
std::shared_ptr<T> | Shared ownership via a reference count | std::make_shared<T>(...) |
std::weak_ptr<T> | Non-owning observer, used to break reference cycles | Created from a shared_ptr |
#include <iostream>
#include <memory>
#include <string>
#include <vector>
struct Shape {
virtual ~Shape() = default;
virtual double area() const = 0;
};
struct Circle : Shape {
explicit Circle(double r) : r{r} {}
double area() const override { return 3.14159 * r * r; }
double r;
};
int main() {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(1.0));
shapes.push_back(std::make_unique<Circle>(2.0));
for (const auto& s : shapes) std::cout << s->area() << '\n';
// every Circle is destroyed when shapes goes away
auto config = std::make_shared<std::string>("debug");
auto another = config; // reference count is now 2
std::cout << config.use_count() << '\n'; // 2
}Default to std::unique_ptr; use std::shared_ptr only when ownership is genuinely shared. A function that merely uses an object should take a reference (such as const Shape&) or a plain pointer, not a smart pointer.
C++11 added move operations, which transfer a resource instead of duplicating it. A temporary, or an object marked with std::move, can hand over its internal buffer, so an expensive copy becomes a few pointer assignments. std::move itself moves nothing; it is a cast meaning "you may move from this".
#include <iostream>
#include <string>
#include <utility>
#include <vector>
int main() {
std::string big(1'000'000, 'x');
std::vector<std::string> list;
list.push_back(big); // copy: big is untouched
list.push_back(std::move(big)); // move: big's buffer is handed over
// big is now valid but unspecified; assign a new value before relying on it
big = "reused";
std::cout << list.size() << '\n'; // 2
}When returning a local, write return x;, not return std::move(x);: the compiler already applies copy elision or an implicit move. And if a class doesn't manage a resource directly, let the compiler generate its copy and move operations (the Rule of Zero).
Templates are functions and classes parameterized by type; the compiler generates a concrete version for each type you use. The standard containers and algorithms are all templates.
#include <concepts>
#include <iostream>
#include <string>
#include <utility>
#include <vector>
template <typename T>
T max_of(const T& a, const T& b) {
return (a < b) ? b : a;
}
template <typename T>
class Stack {
public:
void push(T value) { items_.push_back(std::move(value)); }
T pop() { T top = std::move(items_.back()); items_.pop_back(); return top; }
bool empty() const { return items_.empty(); }
private:
std::vector<T> items_;
};
// C++20 concepts: only integer types are accepted
template <std::integral T>
T half(T value) { return value / 2; }
int main() {
std::cout << max_of(3, 7) << '\n'; // 7
std::cout << max_of(std::string{"a"}, std::string{"b"}) << '\n'; // b
Stack<int> s;
s.push(1);
s.push(2);
std::cout << s.pop() << ' ' << half(10) << '\n'; // 2 5
// half(1.5); // error: double does not satisfy std::integral
}Template definitions must be visible where they are used, so they usually live in headers. C++20 concepts name a template's requirements and turn pages of instantiation errors into a short, readable message.
std::make_unique, and use std::shared_ptr only for genuinely shared ownership.std::move only permits a move; reassign a moved-from object before relying on it.
0 comments
Sign in · Sign in to leave a comment.
Be the first to comment.