11/2009
48
Perełki C++
Statyczne asercje w C++
www.sdjournal.org
49
M
echanizm statycznych asercji zna-
ny jest już od wielu lat, aczkol-
wiek – jak wynika z doświad-
czeń autora niniejszego tekstu, stosunkowo
niewielu programistów stosuje w praktyce
ten użyteczny idiom. Wydaje się, iż wśród
użytkowników języka C++ panuje powszech-
ne przekonanie, iż statyczne asercje to jakiś
ezoteryczny mechanizm przeznaczony je-
dynie dla autorów nowoczesnych (tudzież
skomplikowanych) bibliotek wykorzystują-
cych takie techniki jak metaprogramowanie
oparte na szablonach czy programowanie ge-
neryczne. W niniejszym artykule postaram
się rozwiać ten mit, pokazując, że statyczne
asercje to bardzo prosty, a zarazem użytecz-
ny mechanizm, który z powodzeniem moż-
na wykorzystać w zwyczajnych, pisanych na
co dzień programach.
Czym jest statyczna asercja?
Mechanizmu asercji nie trzeba chyba przed-
stawiać nikomu, kto miał do czynienia z ję-
zykiem C bądź C++. Makro
assert
to bar-
dzo proste, a jednocześnie potężne narzę-
dzie programistyczne, służące do weryfi-
kacji niezmienników w kodzie źródłowym.
Poprzez niezmiennik należy rozumieć do-
wolny warunek, który w mniemaniu autora
programu powinien być w określonym kon-
tekście spełniony. Jednym z najbardziej po-
pularnych zastosowań makra
assert
w języ-
ku C\C++ jest sprawdzanie, czy dany wskaź-
nik jest niezerowy:
assert( p != NULL );
Podobnych warunków może być nieskończe-
nie wiele. Niepisana zasada mówi, że asercje po-
winny być wykorzystywane tylko i wyłącznie
w celu wyłapywania błędów programisty. Na
przykład, jeśli programista zakłada, że wskaź-
nik przekazywany do funkcji będzie niezero-
wy, lub wartość zwracana z innej funkcji bę-
dzie zawsze parzysta, to tego rodzaju niezmien-
niki można weryfikować (i dokumentować) za
pomocą asercji. W przypadku gdy użytkow-
nik wpisał niepoprawne dane lub system ope-
racyjny nie udostępnił programowi żądanego
zasobu, należy użyć alternatywnych mechani-
zmów obsługi błędów, na przykład: rzucić wy-
jątek. Opisana wyżej zasada wiąże się poniekąd
z rezultatami działania makra
assert
objawia-
jącymi się w przypadku, gdy przekazany do nie-
go warunek nie będzie spełniony. Niespełniona
asercja spowoduje brutalne przerwanie progra-
mu i wypisanie informacji o błędzie (zazwyczaj
podawana jest nazwa pliku z kodem źródło-
wym oraz numer linii, w której wystąpił pro-
blem). Oczywistym jest, iż końcowy użytkow-
nik nie chciałby zobaczyć takiego komunikatu
w sytuacji gdy np. wpisze niepoprawnie sfor-
matowany adres e-mail.
Makro
assert
ma pewną ważną właściwość:
jest ono skonstruowane w taki sposób, że dzia-
ła tylko wtedy, gdy program jest zbudowany
w trybie odpluskwiania (ang. debug mode). W
programach zbudowanych w trybie produk-
cyjnym (ang. release mode) instrukcje
assert
są
pominięte i nie powodują żadnego dodatkowe-
go narzutu.
Kolejną, ważną cechą konstrukcji
assert
jest
fakt, iż działa ona w czasie wykonania progra-
mu. W praktyce oznacza to, że warunki opisane
we wszystkich asercjach będą sprawdzone przez
program dopiero w momencie, kiedy zawiera-
jący je kod będzie wykonywany. Z tego wzglę-
du makro
assert
nazywane jest czasami aser-
cją dynamiczną.
No dobrze... – stwierdzi uważny Czytelnik
– ale czy zawsze potrzebujemy korzystać z asercji
dynamicznych? Odpowiedź brzmi: oczywiście,
że nie! Przecież – myśląc zdroworozsądkowo
– niektóre założenia odnośnie działania progra-
Statyczne asercje w C++
Dynamiczne asercje to idiom powszechnie znany i stosowany, statyczne
asercje są zdecydowanie mniej popularne. Jeśli chciałbyś przekonać
się, do czego służy ten ciekawy mechanizm, zapraszam do lektury
niniejszego artykułu.
Dowiesz się:
• Co to jest statyczna asercja w języku C++;
• Jakie biblioteki oferują mechanizm statycz-
nej asercji;
• Jak własnoręcznie zaimplementować me-
chanizm statycznej asercji.
Powinieneś wiedzieć:
• Podstawy programowania w języku C++.
Poziom
trudności
Łatwe sprawdzanie warunków w czasie kompilacji
Listing 1. Zastosowanie statycznej asercji
dostępnej w ramach pakietu Boost
#include <limits>
#include <boost/static_assert.hpp>
template
<
class
UInt
>
class
Test
{
private
:
BOOST_STATIC_ASSERT
(
(
std
::
numeric_limits
<
UInt
>::
digits
>=
16
)
&&
std
::
numeric_limits
<
UInt
>:
:
is_specialized
&&
std
::
numeric_limits
<
UInt
>:
:
is_integer
&& !
std
::
numeric_limits
<
UInt
>::
is_signed
);
public
:
/* ... */
};
Listing 2. Przykładowe wykorzystanie
mechanizmu standardowej statycznej asercji
template
<
class
T
>
class
Test
{
private
:
static_assert
(
sizeof
(
int
)
<=
sizeof
(
T
),
"T is not big
enough!"
);
};
11/2009
48
Perełki C++
Statyczne asercje w C++
www.sdjournal.org
49
mu znane są już na etapie kompilacji. Powstaje
więc pytanie: czy nie dałoby się sprawdzać tych
warunków właśnie wtedy?
Kluczem do znalezienia odpowiedzi na te py-
tania są właśnie statyczne asercje. Mechanizm ten
działa bardzo podobnie do swojego dynamicz-
nego odpowiednika, tyle że działa na etapie
kompilacji programu.
Zaraz, zaraz... – zastanowi się ponownie uważ-
ny Czytelnik. Ale które warunki można weryfiko-
wać za pomocą statycznych asercji? Pytanie to jest
niewątpliwie bardzo ważne. Odpowiedź brzmi:
statyczne asercje obsługują tylko te warunki, któ-
re są zbudowane z tzw. całkowitych wyrażeń sta-
łych (ang. integral constant expressions). Wyrażenia
te mogą składać się z następujących składników:
• literały całkowite;
• wartości enumeracji;
• stałe obiekty, które zostały zainicjowane w
miejscu definicji;
• parametry szablonów będące liczbami cał-
kowitymi bądź wartościami enumeracji;
• wyrażenia
sizeof
;
• stałe adresy.
W kontekście naszych rozważań na temat sta-
tycznych asercji istotny jest fakt, iż standard języ-
ka C++ narzuca wymóg mówiący o tym, że wy-
rażenia złożone z całkowitych wyrażeń stałych
muszą być ewaluowane na etapie kompilacji pro-
gramu. Dzięki temu statyczne asercje mogą dzia-
łać. Tyle teorii. Czas na odrobinę praktyki.
Dostępne implementacje
mechanizmu statycznych asercji
Wiemy już, czym jest w teorii statyczna asercja,
czas wypróbować to narzędzie w działaniu. Za-
nim jednak drogi Czytelniku zaczniesz zasta-
nawiać się, jak można zaimplementować opisa-
ny wyżej mechanizm, wstrzymaj się przez mo-
ment – najprawdopodobniej zamiast tego bę-
dziesz mógł skorzystać z gotowej, wypróbowanej
implementacji statycznej asercji. W niniejszym
podrozdziale przedstawię dostępne alternatywy
w tym zakresie.
Implementacja, do której stosowania będę
Cię namawiał w pierwszej kolejności, stanowi
część zestawu bibliotek języka C++ znanych ja-
ko Boost (patrz: ramka W Sieci). Boost to narzę-
dzie wypróbowane i dobrze udokumentowane
– korzystając z niego, masz gwarancję wysokiej
jakości, przenośności oraz niezawodności kodu.
Mechanizm statycznej asercji jest dostępny w ra-
mach biblioteki Boost.StaticAssert (patrz: ram-
ka W Sieci) pod postacią makra
BOOST_STATIC_
ASSERT
. W celu skorzystania z tej biblioteki na-
leży dołączyć nagłówek static_assert.hpp. Na Li-
stingu 1 przedstawiony jest przykład zastosowa-
nia statycznej asercji rodem z Boost.
W tym przypadku sprawdzamy, czy para-
metr
UInt
w szablonie
Test
jest typu całkowite-
go ze znakiem i jest w stanie reprezentować cyfrę
zapisaną na co najmniej 16 bitach (nie licząc bi-
tu reprezentującego znak).
Warto w tym miejscu wspomnieć, iż makro
BOOST_STATIC_ASSERT
może być użyte zarów-
no w zakresie przestrzeni nazw, funkcji, szablo-
nu funkcji, klasy oraz szablonu klasy.
Alternatywą dla zestawu bibliotek Boost (w
kontekście statycznych asercji) może być biblio-
teka Loki (patrz: ramka W Sieci). Biblioteka ta
związana jest z książką autorstwa Andrei Ale-
xandrescu pt. Nowoczesne Projektowanie w C++
(tytuł angielski: Modern C++ Design). Pozycja
ta, uznawana w kręgu programistów C++ za kla-
sykę, opisuje szczegółowo szereg nowoczesnych
technik języka C++: między innymi statyczne
asercje. Zaprezentowane w książce mechanizmy
zaimplementowane są w towarzyszącej bibliote-
ce: Loki. Statyczna asercja rodem z Loki dostar-
czona jest w postaci makra
LOKI_STATIC_CHECK
,
zaś ogólny schemat korzystania z niej jest iden-
tyczny jak w przypadku
BOOST_STATIC_ASSERT
.
Biblioteka Loki zawiera w sobie wiele interesują-
cych, wartych uwagi rozwiązań, aczkolwiek nie
jest ona tak aktywnie rozwijana jak Boost – dla-
tego też, jako domyślną implementację mecha-
nizmu statycznej asercji, polecam zdecydowa-
nie Boost.StaticAssert.
Bardzo interesującą alternatywą dla wspo-
mnianych wyżej bibliotek jest... standard
C++0x. W planowanej, nowej odsłonie języ-
ka C++ jego projektanci przewidzieli statycz-
ne asercje jako wbudowany mechanizm języka
w postaci słowa kluczowego
static_assert
.
Składnia standardowej statycznej asercji wyglą-
da następująco:
static_assert( stałe_wyrażenie, komunikat_
błędu );
Przykładowe wykorzystanie tego mechanizmu
przedstawione jest na Listingu 2.
Na nowy standard języka C++ przyjdzie nam
zapewne jeszcze trochę poczekać, aczkolwiek
zadowalające jest to, że twórcy kompilatorów nie
zasypują gruszek w popiele i już dziś, w najnow-
szych wersjach swoich produktów, oferują pro-
gramistom wybrane właściwości C++0x. W mo-
mencie pisania niniejszego artykułu standardo-
wy mechanizm statycznych asercji dostępny jest
w Visual Studio 2010 CTP oraz w GCC (wersja
4.3 oraz wyższe). Odnośniki do wspomnianych
narzędzi można znaleźć w ramce W Sieci. Zain-
teresowanych Czytelników gorąco namawiam
do eksperymentów z tymi kompilatorami.
Własna implementacja
mechanizmu statycznej asercji
Mogą zdarzyć się przypadki, kiedy programista
nie może użyć istniejącej implementacji statycz-
nej asercji. Powody bywają różne: opisane wyżej
biblioteki mogą być niedostępne na docelową
platformę, tudzież polityka firmy realizującej
dany projekt może nie zezwalać na stosowanie
zewnętrznych rozwiązań. W takim przypadku
pozostaje jedynie zakasać rękawy i zaimplemen-
tować mechanizm statycznej asercji własnoręcz-
nie. Wbrew pozorom, nie wymaga to wiele wy-
siłku: przykładowa implementacja (którą z po-
wodzeniem można nazwać rozwiązaniem pro-
dukcyjnym) pokazana jest na Listingu 3.
Treść wspomnianego Listingu powinna być
zapisana w pliku nagłówkowym (np. StaticAs-
sert.hpp); w takiej postaci będzie gotowa do
użycia. Nasza implementacja statycznej aser-
cji opiera się na specjalizacjach szablonu kla-
sy
StaticAsserter
. Szablon ten przyjmuje je-
den parametr typu
bool
i zawiera jedynie pu-
Listing 3. Implementacja mechanizmu
statycznej asercji
#if !defined __STATIC_ASSERT_HPP_
INCLUDED__
#define __STATIC_ASSERT_HPP_INCLUDED__
#if !defined _DEBUG
#define static_assert( expr, msg )
#else
// !defined _DEBUG
namespace
Private
{
template
<
bool
>
struct
StaticAsserter
{
StaticAsserter
(
...
)
{}
};
template
<>
struct
StaticAsserter
<
false
>
;
}
#define static_assert( expr, msg ) \
{\
class ERROR_##msg {}; \
Private::StaticAsserter< ( expr )
!= 0 >( ( ERROR_##msg ) ); \
}
#endif
// !defined _DEBUG
#endif
// __STATIC_ASSERT_HPP_
INCLUDED__
Listing 4. Test prezentowanej implementacji
mechanizmu statycznej asercji
#define _DEBUG
#include "StaticAssert.hpp"
int
main
()
{
const
int
a
=
63
;
const
int
b
= -
1
;
static_assert
(
(
a
%
2
)
==
0
,
A_is_not_
dividable_by_2
);
static_assert
(
b
>=
0
,
B_must_be_greater_
or_equal_zero
);
return
0
;
}
11/2009
50
Perełki C++
sty konstruktor, który przyjmuje argumenty do-
wolnego typu (w tym celu stosujemy wielokro-
pek) – za moment wyjaśnię dlaczego. Specjali-
zacja tego szablonu dla wartości parametru rów-
nej
false
jest tylko zadeklarowana. Szablon jest
umieszczony w przestrzeni nazw
Private
(nie
jest to konieczne, ale ja stosuję taki zabieg jako
wskazówkę mówiącą, że zawarty tutaj kod sta-
nowi szczegół implementacji i nie powinien być
wykorzystywany przez kod kliencki).
Rzućmy okiem na definicję makra
static_
assert
. Na początku definiujemy temporalną
klasę reprezentującą komunikat o błędzie:
class ERROR_##msg {};
W dalszej kolejności tworzona jest instancja
szablonu
StaticAsserter
, przy czym jako pa-
rametr tego szablonu podawane jest wyrażenie
( expr ) != 0
.
expr
musi być oczywiście cał-
kowitym wyrażeniem stałym, aby kompilator
mógł obliczyć je w czasie kompilacji. Jeśli wy-
rażenie to jest niezerowe, to kompilator prze-
kształci je na wartość
true
i użyje podstawo-
wej definicji szablonu (tej z wielokropkiem w
konstruktorze). Konstruktor tej klasy przyj-
muje... cokolwiek, więc bezproblemowo prze-
łknie obiekt zdefiniowanej wcześniej tymcza-
sowej klasy. W rezultacie nie wydarzy się nic.
I o to chodzi! Ciekawe rzeczy zaczną się dziać
w sytuacji, kiedy wyrażenie w warunku bę-
dzie równe
0
: wtedy kompilator użyje specjali-
zacji szablonu klasy
StaticAsserter
dla para-
metru równego
false
. Specjalizacja ta jest nie-
zdefiniowana, więc w rezultacie otrzymamy
komunikat o błędzie, który będzie zawierał nu-
mer linii, w której ten błąd wystąpił (wskazują-
cy, gdzie umieściliśmy statyczną asercję) oraz
– nazwę temporalnej klasy (reprezentującą ko-
munikat o błędzie). Voila! Mamy wszystko cze-
go nam potrzeba!
Oczywiście, wystarczy zmienić wartości
stałych
a
i
b
, przykładowo na:
64
i
1
, a kom-
pilacja przejdzie bezbłędnie. Gdyby zachciało
nam się oszukać kompilator i np. usunąć sło-
wo kluczowe
const
z definicji stałej
a
, reak-
cja obydwu narzędzi będzie natychmiastowa.
Dla przykładu, w komunikacie o błędzie wy-
generowanym przez kompilator GCC pojawi
się informacja:
StaticAssertTest.cpp:10: error: `a’ cannot appear in
a constant-expression
Statyczne asercje: studium zastosowań
Wiemy już, co to jest statyczna asercja, jak ją
stosować, a nawet – jak ją zaimplementować.
Pozostaje ostatnie – chyba najbardziej klu-
czowe pytanie: w jakim celu, tudzież w jakich
przypadkach stosować ten mechanizm. W ni-
niejszym podpunkcie postaram się na to pyta-
nie odpowiedzieć.
Pierwsza, najbardziej chyba oczywista
wskazówka odnośnie stosowania statycznych
asercji brzmi: za każdym razem, kiedy wsta-
wiasz w kodzie dynamiczną asercję, sprawdź,
czy wyrażenie warunkowe nie jest przypad-
kiem całkowitym wyrażeniem stałym. Jeśli
nie, to rozważ, czy nie da się przekształcić go
na takie wyrażenie.
Pozwolę sobie przytoczyć w tym miejscu przy-
kład z mojej własnej praktyki. Miałem okazję pra-
cować niedawno nad implementacją gry bazują-
cej na tzw. kafelkach (ang. tiles). Mechanizm ka-
felkowania przy programowaniu gier oznacza, że
wybrane elementy sceny gry (np. tło) składane są
przy renderowaniu z ograniczonej liczby elemen-
tów podstawowych. Element taki, zwany kaflem
(ang. tile), to zazwyczaj prostokątny fragment ob-
razu, który kopiuje się na ekran. W moim przy-
padku szerokość i wysokość kafla były stałe. Zde-
finiowałem je w mniej więcej następujący sposób:
const int k_tileWidth = 32;
const int k_tileHeight = 32;
Oprócz tego, przyjąłem założenie, iż wielko-
ści te muszą być podzielne przez liczbę 8; na
tym założeniu opierał się algorytm przewijania
planszy gry. Zacząłem się zastanawiać, jak wy-
musić spełnienie tego warunku – na wypadek
gdybym chciał w przyszłości zmienić rozmiar
kafelka. Asercja wydała mi się idealnym roz-
wiązaniem, szybko jednak zdałem sobie spra-
wę, że warunek ten można zweryfikować na
etapie kompilacji mojego programu. W efekcie
napisałem kod podobny do tego, który pokaza-
ny jest na Listingu 5.
Oczywiście, poprzestając jedynie na próbach
przekształcania dynamicznych asercji na ich sta-
tyczne odpowiedniki, nie uda nam się wykorzy-
stać pełnego potencjału nowo poznanego na-
rzędzia. Aby okiełznać moc statycznych aser-
cji, warto zainteresować się ciekawym mecha-
nizmem, służącym do badania typów oraz re-
lacji występujących między nimi. Mowa tutaj
o tzw. cechach typów (ang. type traits). Mecha-
nizm ten, dostępny w nowoczesnych bibliote-
kach C++ (np. wspomniane wcześniej Boost i
Loki), pozwala sprawdzać w czasie kompilacji
niebanalne cechy typów, np.: czy dany typ jest
typem bazowym dla innego typu, czy posiada
konstruktor kopiujący czy może jest wskaźni-
kiem do metody klasy. Czytelników zaintere-
sowanych możliwościami tego narzędzia odsy-
łam do dokumentacji biblioteki Boost.TypeTra-
its (patrz: ramka W Sieci). W kontekście niniej-
szego artykułu najbardziej istotne jest to, że ce-
chy typów można testować na etapie kompilacji,
a co za tym idzie – nakładać na nie ograniczenia
przy pomocy statycznych asercji!
Statyczne asercje będą niewątpliwie nieoce-
nioną pomocą dla osób piszących programy nie-
zależne od platformy: makro
static_assert
nieraz może uratować je przed trudnymi do
wykrycia błędami wynikającymi z niezgodno-
ści reprezentacji typów stosowanych przez róż-
ne kompilatory.
Reasumując podpunkt o zastosowaniach sta-
tycznych asercji, można by pokusić się o stwier-
dzenie: grunt to zacząć ich używać, a wtedy oka-
zje do ich stosowania zaczną pojawiać się same.
Podsumowanie
W powyższym artykule przedstawiłem defini-
cję mechanizmu statycznych asercji, zaprezen-
towałem najpopularniejsze jego implementa-
cje, opisałem implementację własną, zaś na koń-
cu – omówiłem krótkie studium użycia tego po-
żytecznego narzędzia. Gorąco zachęcam Czytel-
ników do stosowania zaprezentowanego idiomu
w celu usprawnienia własnych programów.
Listing 5. Zastosowanie statycznych asercji do
wymuszenia odpowiednich rozmiarów kafelka
static_assert
(
(
k_tileWidth
%
8
)
==
0
,
Tile_width_is_not_
dividable_by_8
);
static_assert
(
(
k_tileHeight
%
8
)
==
0
,
Tile_height_is_not_
dividable_by_8
);
W Sieci
• http://www.boost.org/ – strona domowa
pakietu bibliotek Boost;
• http://www.boost.org/doc/libs/1_40_0/
doc/html/boost_staticassert.html – doku-
mentacja biblioteki Boost.StaticAssert;
• http://loki-lib.sourceforge.net/ – strona
domowa biblioteki Loki;
• ht t ps : //co n n e c t . m i c roso f t . co m/
VisualStudio/content/content.aspx?Conte
ntID=9790 – strona domowa Visual Stu-
dio 2010 CTP;
• http://gcc.gnu.org/ – strona domowa
projektu GCC;
• http://www.boost.org/doc/libs/1_40_0/
libs/type_traits/doc/html/index.html – do-
kumentacja biblioteki Boost.TypeTraits.
RAFAŁ KOCISZ
Pracuje na stanowisku Dyrektora Technicznego w
firmie Gamelion, wchodzącej w skład Grupy BLStre-
am. Rafał specjalizuje się w technologiach związa-
nych z produkcją oprogramowania na platformy
mobilne, ze szczególnym naciskiem na tworzenie
gier. Grupa BLStream powstała, by efektywniej wy-
korzystywać potencjał dwóch szybko rozwijających
się producentów oprogramowania – BLStream i
Gamelion. Firmy wchodzące w skład grupy specjali-
zują się w wytwarzaniu oprogramowania dla klien-
tów korporacyjnych, w rozwiązaniach mobilnych
oraz produkcji i testowaniu gier.
Kontakt z autorem: rafal.kocisz@game-lion.com