Använder du smarta pekare i C++?

I förra artikeln diskuterade jag rule of zero och de speciella medlems-funktionerna i C++. Där såg vi hur resursägande klasser kan utformas så att de blir robusta och enkla att använda. Ett naturligt nästa steg är att titta närmare på standardbibliotekets färdiga lösningar för just resursägande: de smarta pekarna.
Vad är smarta pekare?
Smarta pekare i C++ är klasser som beter sig som vanliga pekare men samtidigt automatiskt hanterar livstiden för den resurs de pekar på. De frigör minnet när det inte längre används och minskar därmed risken för minnesläckor, dubbel delete eller dangling pointers. Man kan se dem som konkreta exempel på resource management classes, som inkapslar både pekaren och dess ansvar.
Låt oss börja med att skriva en mycket enkel smart pekare – och genast se varför den är felaktig:
template<typename T>
class SimplePtr {
T* addr = nullptr;
public:
SimplePtr() = default;
explicit SimplePtr(T* addr_) : addr(addr_) {}
~SimplePtr() { delete addr; }
auto get() { return addr; }
auto operator->() -> T* { return addr; }
auto operator*() -> T& { return *addr; }
explicit operator bool() const { return addr != nullptr; }
};
Vid första anblicken borde detta fungera. Här använder vi den att hantera ett person-objekt.
auto ptr = SimplePtr<Person>{ new Person{"Per Silja", 42} };
if (ptr) (*ptr).incr_age();
std::cout << "ptr: " << ptr->to_string() << "\n";
To delete or not to delete; that's the question
Säg att vi bygger vidare på detta och skapar en bil-klass, som håller reda på bilens ägare.
class Person {
string name;
unsigned age;
public:
Person(string const& name_, unsigned age_)
: name(name_), age(age_) {}
auto to_string() {
return std::format("Person({}, {})", name, age);
}
};
class Car {
std::string license_no;
SimplePtr<Person> owner;
public:
Car(std::string const& licno, Person* owner_)
: license_no{licno}, owner{ owner_ } {}
auto to_string() {
return std::format("Car({}, owner: {})",
license_no, owner->to_string());
}
};
int main() {
auto nisse = SimplePtr<Person>{ new Person{"Nisse Hult", 53} };
std::cout << nisse->to_string() << "\n";
{
auto volvo = Car{"ABC123", nisse.get()};
std::cout << volvo.to_string() << "\n";
}
std::cout << nisse->to_string() << "\n";
}
Kompilerar och kör vi programmet, blir det så här
C++> g++ -std=c++23 -Wall ../simple-ptr.cxx -o simple-ptr && ./simple-ptr
Person(Nisse Hult, 53)
Car(ABC123, owner: Person(Nisse Hult, 53))
Segmentation fault
C++>
Vad hände här, programmet kraschade? 😮
I det inre blocket har vi två pekare till samma minnesblock. När programmet lämnar blocket körs destruktorn för objektet volvo. Denna har ett medlemsobjekt (owner), varpå destruktorn för denna anropas, vilken destruerar Nisse Hult. Efter blocket pekar nisse på ett borttaget minnesblock med följd att programmet kraschar.
Problemet här är att vi inte har gjort en klar distinktion mellan ägande respektive lånande. I det första fallet så finns ansvaret att frigöra resursen men inte i det andra. Anropet av nisse.get()
till konstruktorn för Car
är det problematiska.
Berättelsen om std::auto_ptr (The rise and fall of auto_ptr)
I slutet av 90-talet saknade C++ både flyttsemantik och standardiserade smarta pekare. RAII-mönstret var etablerat, men många programmerare “glömde” delete vid tidiga returvägar eller kastade undantag och lämnade läckor efter sig. std::auto_ptr kom in i C++98 som en enkel RAII-wrapper för att automatiskt anropa delete när pekaren gick ur scope. RAII står för Resource Acquisition Is Initialisation och innebär att en resurs förvärvas i konstruktorn och frigörs i destruktorn.
Grundidé och API i korthet
- Äger exakt ett objekt allokerat med new.
- Kopiering överför ägande (källan nollställs); destruktorn anropar delete.
- Typiska medlemmar: get(), release(), reset(); det fanns även en intern hjälptyp auto_ptr_ref för att få kopiering från temporärer att fungera.
Ett minimalt användningsexempel:
std::auto_ptr<int> p( new int(42) );
std::auto_ptr<int> q = p; // q tar över, p blir NULL
Efter raden ovan är p tom. Detta var avsiktligt.
Designen som gick snett
1) “Kopiera” betyder “flytta”
Eftersom språket saknade flytt, lät man kopiering betyda ägarflytt. Det bröt mot intuitiva förväntningar och mot krav/antaganden i många generiska algoritmer. Exempelvis blir anrop som tar argument per värde ägarflyttar – ibland utan att läsaren inser det.
void take(std::auto_ptr<int> x); // tar över ägande
//...
std::auto_ptr<int> p(new int(1));
take(p); // p blir nullptr efter anropet
2) Otillåtet i standardcontainrar
Eftersom kopiering modifierar högeroperanden uppfyller auto_ptr inte kraven för CopyConstructible på det sätt som standardbibliotekets containrar och algoritmer förutsätter. Därför förbjöds auto_ptr i standardcontainrar.
std::vector<std::auto_ptr<int>> v; // ill-formed enligt standarden
3) Fel delete för arrayer
auto_ptr anropar alltid delete, aldrig delete[]. Att stoppa en array i auto_ptr är därför undefined behavior. (I modern C++ använder man std::unique_ptr<T[]> eller behåller arrayer i std::vector
Vägen mot utfasning
2007: unique_ptr antas, auto_ptr markeras för utfasning
På WG21-mötet i Oxford 2007 accepterades förslaget (N1856) som introducerade std::unique_ptr. Samtidigt sattes riktningen: auto_ptr skulle fasas ut. Flyttsemantik (C++11) gjorde det möjligt att få tydlig ägarflytt via std::move – utan att missbruka kopiering.
C++11: auto_ptr deprecieras
I och med C++11 blev auto_ptr deprecated. Rekommendationen blev att migrera till std::unique_ptr (exklusivt ägande) eller std::shared_ptr (delat ägande) där så behövs.
C++17: auto_ptr tas bort
Arbetet formaliserades i bl.a. N4190 (“Removing auto_ptr, random_shuffle, …”) och ledde till att auto_ptr togs bort ur standardbiblioteket i C++17. Implementatörer kunde under en övergångsperiod erbjuda återaktivering via kompatibilitetsflaggor, men i standardspråket är typen borta.
Smarta pekare i C++11
Med standarden C++11, vilken också kom att markera startpunkten för Modern C++, infördes två smarta pekartyper.
std::unique_ptr<T>
- Exklusivt ägarskap, vilket kan flyttas med std::movestd::shared_ptr<T>
- Delat ägarskap med referensräknare
Om du redan är bekant med dessa, så kanske du har noterat att det finns den tredje typ som heter std::weak_ptr. Denna är helt kopplad till shared_ptr och används för att undertrycka referens-räkningen, vilket kan behövas i vissa fall. Jag avser inte att diskutera denna mer i denna artikel.
std::unique_ptr<T>
- Exklusivt, men flyttbart, ägarskap
Vi börjar med unique_ptr, som bör vara ditt förstahands val. Enbart om det inte går att använda denna av applikationsskäl ska du använda den andra formen shared_ptr.
Så här kan du skapa en initierad pekare och sen flytta ägarskapet till en annan.
#include <memory>
//...
auto p1 = std::unique_ptr<Person>{ new Person{"Anna Conda", 37} };
auto p2 = std::unique_ptr<Person>{};
p2 = std::move( p1 );

Det finns också en fabriks-funktion, som kan vara enklare att använda vid initiering. Notera nedan att den tar konstruktor-argument för målklassen.
auto p = std::make_unique<Person>("Per Silja", 42);
Liknande SimplePtr ovan, så är flera pekar-orienterade operatorer överlagrade.
void something(std::unique_ptr<Person> p) {
if (p) { //operator bool()
(*p).incr_age(); //operator *()
std::cout << "p: " << p->get_name() << "\n"; //operator ->()
}
}
int main() {
auto p = std::make_unique<Person>("Per Silja", 42);
something( std::move(p) );
}
Det går att "låna" pekaren med metoden get()
. Det går att byta ut pekaren med reset()
eller swap()
. Samt, det går att lämna över ägarskapet av den råa pekaren med release()
.
auto p1 = std::unique_ptr<Person>{ new Person{"Anna Conda", 37} };
auto p2 = std::unique_ptr<Person>{ new Person{"Per Silja", 42} };
p1.swap(p2);
auto* ptr = p1.release();
delete ptr;
std::shared_ptr<T>
- Delat, och kopierbart, ägarskap
Med shared_ptr får vi en begränsad form av skräpsamling (garbage collection), i och med att den använder referensräkning.
Så här kan du skapa en initierad pekare och sen dela ägarskapet med en annan. Notera att du denna gång inte använder std::move.
#include <memory>
//...
auto p1 = std::shared_ptr<Person>{ new Person{"Anna Conda", 37} };
auto p2 = std::shared_ptr<Person>{};
p2 = p1;

Även denna typ har en fabriksfunktion du kan använda, som alternativ för att skapa en initierad pekare.
auto p = std::make_shared<Person>("Justin Time", 53);
Utbudet av metoder och operatorer är hyfsat likt det för unique_ptr, såsom get(), operator*(), operator->(), operator bool(), swap och reset(). Dock ingen release(). Det som skiljer är att denna har en referensräknare, vilken man kan läsa av med metoden use_count().
#include <print>
#include <string>
#include <memory>
using std::string;
class Person {
string name;
unsigned age;
public:
Person(string const& name_, unsigned age_)
: name(name_), age(age_) {}
auto incr_age() { return ++age; }
auto to_string() {
return std::format("Person({}, {})", name, age);
}
};
void something(std::shared_ptr<Person> p) {
p->incr_age();
std::println("something: {}, #ref={}", p->to_string(), p.use_count());
}
int main() {
auto ptr1 = std::make_shared<Person>("Per Silja", 51);
std::println("1. main: {}, #ref={}", ptr1->to_string(), ptr1.use_count());
{
auto ptr2 = std::shared_ptr<Person>{};
ptr2 = ptr1;
ptr2->incr_age();
std::println("2. main: {}, #ref={}", ptr2->to_string(), ptr2.use_count());
something(ptr2);
std::println("3. main: {}, #ref={}", ptr2->to_string(), ptr2.use_count());
}
std::println("4. main: {}, #ref={}", ptr1->to_string(), ptr1.use_count());
}
1. main: Person(Per Silja, 51), #ref=1
2. main: Person(Per Silja, 52), #ref=2
something: Person(Per Silja, 53), #ref=3
3. main: Person(Per Silja, 53), #ref=2
4. main: Person(Per Silja, 53), #ref=1
Process finished with exit code 0
Custom Deleter
Standard-beteendet när en smart pekare frigör sin resurs är att anropa delete
, eftersom antagandet är att minnesblocket allkokerades med new
. Emellertid, om allokeringen utfördes av malloc
, så är det starkt rekommendabelt att deallokeringen utförs av free
. Det sistnämnda kan realiseras av en så kallad custom deleter, vilket är en struct med en function-call operator, som tar en pekare som argument. Här kommer ett belysande exempel.
#include <print>
#include <memory>
#include <cstring>
typedef struct {
unsigned type;
unsigned char payload[128];
} Parcel;
Parcel* create_parcel(unsigned type, char padding) {
Parcel* p = (Parcel*)malloc(sizeof(Parcel));
std::println("[create_parcel] addr={:#x}", reinterpret_cast<uintptr_t>(p));
p->type = type;
::memset(&(p->payload), padding, sizeof(p->payload));
return p;
}
struct FreeDeleter {
void operator()(void* addr) {
std::println("[free-deleter] addr={:#x}", reinterpret_cast<uintptr_t>(addr));
::free(addr);
}
};
int main() {
std::println("[main] enter");
{
auto parcel = std::unique_ptr<Parcel, FreeDeleter>{ create_parcel(42, 'A') };
std::println("[main] type: {}, pad-char: '{}'",
parcel->type, static_cast<char>(parcel->payload[0]));
}
std::println("[main] exit");
}
C++> g++ -std=c++23 -Wall ../custom-deleter.cxx -o custom-deleter
C++> valgrind --leak-check=yes ./custom-deleter
Memcheck, a memory error detector
...
[main] enter
[create_parcel] addr=0x4ebe4c0
[main] type: 42, pad-char: 'A'
[free-deleter] addr=0x4ebe4c0
[main] exit
HEAP SUMMARY:
in use at exit: 0 bytes in 0 blocks
total heap usage: 6 allocs, 6 frees, 74,977 bytes allocated
All heap blocks were freed -- no leaks are possible
...
C++>
Stateless vs. Stateful Deleter
Det finns två former av deleters, beroende på om hur frigöringen av resursen ska utföras. Den första är stateless, vilket exemplet ovan illustrerar bra. Det krävs ingen extra information för att frigöra ett minnesblock med free
. Av det skälet räcker det med att bifoga typen för deletern, vilken kommer att instansieras internt i pekaren.
Alternativet är en stateful deleter, vilket behövs när man måsta hantera extra information för att frigöra en resurs, typiskt för att lämna tillbaka till en viss pool. Förutom deleter-typen, behöver man också skicka in ett dylikt skapat objekt tillsammans med den råa pekaren.
auto ptr = std::unique_ptr<Type, Deleter>{new Type{...}, Deleter{...}};
Här följer ett komplett och belysande exempel på användning av en pool med minnesblock tillsammans med unique_ptr. Själva poolen är enklast tänkbara, den består av ett block med bytes plus en boolsk array för att hålla reda på vilka block som är upptagna. Sen kommer en deleter, som lämnar tillbaka ett block till en viss pool. För att underlätta har vi ett typ-alias plus en fabriks-function för att skapa en dylik pekare korrekt konfigurerad. Till slut, har vi själva demo-programmet som består av en dummy typ med spårutskrifter, samt main där en vector populeras med objekt.
#include <print>
#include <array>
#include <vector>
#include <algorithm>
#include <ranges>
#include <memory>
namespace r = std::ranges;
template<typename ElemType, unsigned CAPACITY = 5>
class ObjectPool {
std::array<char, CAPACITY * sizeof(ElemType)> storage;
std::array<bool, CAPACITY> busy{};
public:
auto alloc() -> ElemType* {
auto it = r::find_if(busy, [](bool x){ return x==false; });
auto index = std::distance(busy.begin(), it);
busy[index] = true;
auto addr = storage.data() + index * sizeof(ElemType);
return reinterpret_cast<ElemType*>(addr);
}
void dispose(ElemType* obj) {
auto index = std::distance(reinterpret_cast<ElemType*>(storage.data()), obj);
busy[index] = false;
}
auto size() const { return r::count_if(busy, [](bool x){ return x==true; }); }
bool empty() const { return size() == 0; }
bool full() const { return size() == CAPACITY; }
};
template<typename ElemType, unsigned CAPACITY = 5>
struct Deleter {
ObjectPool<ElemType, CAPACITY>& pool;
Deleter(ObjectPool<ElemType, CAPACITY>& pool) : pool(pool) {}
void operator()(void * addr) const {
auto ptr = reinterpret_cast<ElemType*>(addr);
ptr->~ElemType(); //invoke its destructor
pool.dispose(ptr);
}
};
template<typename ElemType, unsigned CAPACITY = 5>
using PoolPtr = std::unique_ptr<ElemType, Deleter<ElemType, CAPACITY>>;
template<typename ElemType, unsigned CAPACITY, typename... ConstrArgs >
auto make_pool_ptr(ObjectPool<ElemType, CAPACITY>& pool, ConstrArgs... args) {
auto ptr = new (pool.alloc()) ElemType{args...};
return PoolPtr<ElemType, CAPACITY>{ptr, Deleter{pool}};
}
Här följer själva demo programmet
struct Data {
long double value{};
Data() {
std::println("CREATE Data() @ {:x}",
reinterpret_cast<unsigned long>(this));
}
Data(long double v) : value(v) {
std::println("CREATE Data({}) @ {:x}", value,
reinterpret_cast<unsigned long>(this));
}
~Data() {
std::println("DISPOSE Data({}) @ {:x}", value,
reinterpret_cast<unsigned long>(this));
}
Data(Data const&) = delete;
};
int main() {
auto pool = ObjectPool<Data>{};
{
auto objs = std::vector<PoolPtr<Data>>{};
for (auto k = 1; not pool.full(); ++k) {
auto ptr = make_pool_ptr(pool, k * 10.L);
objs.emplace_back(std::move(ptr));
}
std::println("pool.size = {}", pool.size());
for (auto const& obj: objs) {
std::println("obj: {}", obj->value);
}
}
std::println("pool.size = {}", pool.size());
}
Kompilerar vi och kör programmet via valgrind, kan det se ut på följande sätt.
C++> g++ -std=c++23 -Wall ../custom-deleter-2.cxx -o custom-deleter-2
C++> valgrind --leak-check=yes ./custom-deleter
...
[main] enter
[create_parcel] addr=0x4ebe4c0
[main] type: 42, pad-char: 'A'
[free-deleter] addr=0x4ebe4c0
[main] exit
HEAP SUMMARY:
in use at exit: 0 bytes in 0 blocks
total heap usage: 6 allocs, 6 frees, 74,977 bytes allocated
All heap blocks were freed -- no leaks are possible
...
C++>
Objekt-Orientering och Pekare
C++ är ett språk med stark typing och med fokus på egen-definierade datatyper, på så sätt att instanser av en klass, skulle tas för att vara inbyggda i språket, vilket innebär värde-semantik (value semantics). Detta till skillnad från språk med referens-semantik, såsom Java, där instanser utgörs av pekare till heap-allokerade objekt i kombination med automatisk skräp-objekts insamling (garbage collection).
Det här får stora konsekvenser vid design av program med klass-hierarkier. I ett språk som Java kan man enkelt fylla upp en vektor med pekare till objekt av olika subklasser. Emellertid, fungerar detta inte på samma sätt i C++.
I min förra artikeln om Rule-of-Zero, diskuterade jag vikten av att en polymorf basklass måste
- ha en virtuell destruktor
- vara icke flytt- eller kopierbar
Låt oss börja med en klassiker, som du säkerligen har sett förut; klassen Shape med några subklasser. Denna uppfyller kraven ovan.
#pragma once
#include <print>
#include <string>
#include <numbers>
#include <random>
using std::string;
struct Shape {
string const descr;
Shape(string const& n) : descr(n) {
std::println("CREATE {} @ {:x}", descr, reinterpret_cast<unsigned long>(this));
}
virtual ~Shape() {
std::println("DISPOSE {} @ {:x}", descr, reinterpret_cast<unsigned long>(this));
}
Shape(Shape const&) = delete;
Shape(Shape&&) noexcept = delete;
auto operator =(Shape const&) -> Shape& = delete;
auto operator =(Shape&&) noexcept -> Shape& = delete;
virtual auto area() const -> double = 0;
};
struct Rect : Shape {
int w, h;
Rect(int w, int h) : Shape(std::format("Rect({}, {})", w, h)), w(w), h(h) {}
auto area() const -> double override { return w * h; }
};
struct Circ : Shape {
int r;
Circ(int r) : Shape(std::format("Circ({})", r)), r(r) {}
auto area() const -> double override { return std::numbers::pi * r * r; }
};
struct Square : Shape {
int s;
Square(int s) : Shape(std::format("Square({})", s)) , s(s) {}
auto area() const -> double override { return s * s; }
};
inline auto create_shape(int k = -1) -> Shape* {
static auto r = std::default_random_engine{};
if (k == -1) {
auto I = std::uniform_int_distribution{0, 2};
k = I(r);
}
auto D = std::uniform_int_distribution{1, 10};
switch (auto rc = k % 3; rc) {
case 0: return new Rect{D(r), D(r)};
case 1: return new Circ{D(r)};
case 2: return new Square{D(r)};
default: throw std::runtime_error{"oops, should not happen"};
}
}
En vanlig bug är att populera en container med pekare till subklass objekt, och sen missa att frigöra dessa. Här kommer ett belysande program-exempel.
#include <print>
#include <vector>
#include "shapes.hxx"
int main() {
std::println("[main] enter");
auto N = 5;
{
auto shapes = std::vector<Shape *>{};
for (auto k = 1; k <= N; ++k) shapes.push_back(create_shape());
for (auto&& s : shapes) std::println("{:<12}: {:>5.1f}", s->descr, s->area());
}
std::println("[main] exit");
}
C++> g++ -std=c++23 -Wall ../shapes-demo-1.cxx -o shapes-demo-1
C++> valgrind --leak-check=yes ./shapes-demo-1 C++> g++ -std=c++23 -Wall ../shapes-demo-1.cxx -o shapes-demo-1
...
[main] enter
CREATE Rect(2, 8) @ 4ec14c0
CREATE Circ(6) @ 4ec15e0
CREATE Rect(1, 7) @ 4ec1700
CREATE Square(10) @ 4ec1830
CREATE Circ(6) @ 4ec1900
Rect(2, 8) : 16.0
Circ(6) : 113.1
Rect(1, 7) : 7.0
Square(10) : 100.0
Circ(6) : 113.1
[main] exit
...
HEAP SUMMARY:
in use at exit: 240 bytes in 5 blocks
total heap usage: 21 allocs, 16 frees, 75,422 bytes allocated
...
LEAK SUMMARY:
definitely lost: 240 bytes in 5 blocks
Hoppsan då, här missade vi visst att anropa delete
. Det är ju precis detta som smarta pekare hjälper oss med. Här kommer en ny version, där vi populerar en vector med objekt hanterade av unique_ptr.
#include <print>
#include <vector>
#include <memory>
#include "shapes.hxx"
int main() {
std::println("[main] enter");
auto N = 5;
{
auto shapes = std::vector< std::unique_ptr<Shape> >{};
for (auto k = 1; k <= N; ++k) shapes.emplace_back(create_shape());
for (auto&& s : shapes) std::println("{:<12}: {:>5.1f}", s->descr, s->area());
}
std::println("[main] exit");
}
C++> g++ -std=c++23 -Wall ../shapes-demo-2.cxx -o shapes-demo-2
C++> valgrind --leak-check=yes ./shapes-demo-2
...
[main] enter
CREATE Rect(2, 8) @ 4ec24c0
CREATE Circ(6) @ 4ec25e0
CREATE Rect(1, 7) @ 4ec2700
CREATE Square(10) @ 4ec2830
CREATE Circ(6) @ 4ec2900
Rect(2, 8) : 16.0
Circ(6) : 113.1
Rect(1, 7) : 7.0
Square(10) : 100.0
Circ(6) : 113.1
DISPOSE Rect(2, 8) @ 4ec24c0
DISPOSE Circ(6) @ 4ec25e0
DISPOSE Rect(1, 7) @ 4ec2700
DISPOSE Square(10) @ 4ec2830
DISPOSE Circ(6) @ 4ec2900
[main] exit
...
HEAP SUMMARY:
in use at exit: 0 bytes in 0 blocks
total heap usage: 26 allocs, 26 frees, 75,577 bytes allocated
All heap blocks were freed -- no leaks are possible
Slutsats
Smarta pekare realiserar i praktiken de principer vi såg i rule of zero. De flyttar ansvaret för resurshantering från programmeraren till biblioteket och gör koden både säkrare och mer lättläst. Samtidigt finns det situationer där råa pekare fortfarande är rätt val – till exempel för icke-ägande observationer. Den viktiga skillnaden i modern C++ är att vi alltid är tydliga med vem som äger vad.