Welcome here! | Articles | Main projects | About me
(FR) La const-correctness appliquée au C++ moderne
Sommaire
Introduction
En C++, on parle souvent de const-correctness. Il s’agit d’une bonne pratique visant à promouvoir l’usage de constantes autant que possible. Cela nous permet d’éviter de modifier des variables qui ne devraient pas l’être.
Prenons l’exemple suivant :
// cela marche avec toute fonction renvoyant un code d'erreur,
// mais SDL_BlitSurface a l'avantage d'être bien connue.
int SDL_BlitSurface(SDL_Surface* src,
const SDL_Rect* srcrect,
SDL_Surface* dst,
SDL_Rect* dstrect);
int result = SDL_BlitSurface(src, srcrect, dst, dstrect);
if (result = 0) { // /!\ Erreur : un '=' ou un '!' a été oublié /!\
// ^ ici
}
Que se passe-t-il ici ? La variable result
, quelle que soit sa valeur, est réassignée dans le if
de manière à valoir 0. Ce faisant :
- La condition est toujours évaluée à
false
; - La précédente valeur de
result
est écrasée.
Ainsi, nous ne savons pas si l’appel à SDL_BlitSurface
a réussi ou échoué. Une solution possible serait de passer la variable result
constante de manière à avoir une erreur de compilation :
error: cannot assign to variable 'result' with const-qualified type 'const int'
Certains langages, notamment en programmation fonctionnelle, rendent par défaut toutes les variables constantes :
let foo = 42;; (* 'foo' n'est pas modifiable *)
En C++, nous avons de nombreux moyens de déclarer des constantes. C++11 a par ailleurs ajouté le mot-clé constexpr
, que je vais comparer aux habituels const
et #define
.
constexpr
vs const
Comme vu dans la partie précédente, const
permet de déclarer une variable constante :
const int i = 42; // i n'est pas modifiable
Le mot-clé const
peut être placé avant ou après le type, sans réelle incidence :
const int i = 42;
// équivaut à :
int const i = 42;
Une petite subtilité existe cependant, dans le cas des pointeurs :
const int *ptr = 42;
// n'est pas la même chose que :
int* const ptr = 42;
Dans le premier cas, la valeur pointée par ptr
n’est pas modifiable. Il n’est donc pas possible d’assigner une valeur à *ptr
.
Dans le second cas, le pointeur n’est pas modifiable, mais on peut assigner une valeur à *ptr
.
Note - Pour déclarer un pointeur constant vers une valeur constante, on utilise const T* const
.
Arrivé avec la norme C++11, le mot-clé constexpr
permet quant à lui de déclarer des constantes évaluées à la compilation. Cependant, leurs valeurs doivent pouvoir être évaluées à la compilation. Un tel code n’est donc pas valide :
int value() {
int i = 0;
std::cin >> i;
return i;
}
// à l'usage :
const auto i1 = value(); // ok
constexpr auto i2 = value(); // pas ok
Mais du coup, si constexpr
est plus contraignant que const
, quel est son intérêt ?
Outre le fait qu’une variable constexpr
est forcément une constante de compilation, on peut également y appliquer un template :
template<int N>
constexpr auto square = N * N;
// mais encore :
template<class T, T N>
constexpr T square = N * N;
C’est par ailleurs ainsi que sont définies des variables telles que std::is_same_v
(qui n’est en définitive qu’un alias pour std::is_same<T, U>::value
).
Enfin, une fonction peut également être constexpr
!
constexpr size_t fac(size_t value) noexcept {
return (value == 0) ? 1 : value * fac(value - 1);
}
// et à l'usage :
constexpr auto fac5 = fac(5);
Et, mieux encore, on peut également créer une fonction constexpr
avec un template !
constexpr
vs #define
Il existe également la version paléolithique “à l’ancienne” :
#define SOME_CONSTANT 42
Toutes les ocurrences de SOME_CONSTANT
seront remplacées par la valeur associée, ici 42
.
Néanmoins, l’option #define
pose un certain nombre de problèmes :
Dans l’exemple ci-dessus, SOME_CONSTANT
doit être déclarée dans le scope global ; on ne peut donc pas redéfinir une variable avec le même nom. Ce soucis n’existe pas avec constexpr
:
namespace foo {
constexpr int CONSTANT = 42;
} // namespace foo
namespace bar {
constexpr int CONSTANT = 12;
} // namespace bar
// et à l'usage :
const auto foo_const = foo::CONSTANT;
const auto bar_const = bar::CONSTANT;
Ce qui induit que le code suivant est valide :
namespace foo {
#define FOO 42
} // namespace foo
int main() {
std::cout << FOO; // 42
}
De plus, une constante déclarée de cette manière n’est pas explicitement typée :
#define PI 3.14159265359
Version constexpr
:
constexpr double PI = 3.14159265359;
// ou :
template<class T>
constexpr T PI = static_cast<T>(3.14159265359);
Comme vu plus haut, on peut appliquer un template à une variable constexpr
. Cela permet de réaliser des calculs évalués au compile-time, ce qui n’est pas nécessairement le cas avec un #define
:
template<int N>
constexpr auto square = N * N; // sera forcément évalué à la compilation
#define SQUARE(N) N * N // ?
En C++ moderne, les constantes à base de #define
devraient être évitées au profit de constexpr
.
Conclusion
D’une manière générale, j’ai tendance à n’utiliser #define
que pour des include guards. Parallèlement, j’utilise autant que possible des variables constantes afin d’éviter des effets de bord. Lorsque je peux utiliser constexpr
, alors j’utilise constexpr
. À défaut, j’utilise const
.
C++ met à disposition des outils pour optimiser au mieux notre code, alors autant les utiliser.