Máté Cserép
October 2015, Budapest
Constant expressions (constexpr)
/* Values */
int i = 4; // not constant, can be modified:
i = 5;
const int ci = 6; // constant, must be initialized
ci = 7; // syntax error, cannot be modified
/* References */
int &ir1 = i; // reference, must be initialized
ir1 = 6;
int &ir2 = ci; // syntax error, const correctness would be flawed
const int &cir1 = ci; // constant reference to a constant value
cir1 = 7; // syntax error
const int &cir2 = i; // const reference to a non-const value, still ok
cir2 = 7; // syntax error, cir2 is const reference
/* Pointers */
int *ip;
ip = &i;
*ip = 5; // ok
ip = &ci; // syntax error, const correctness would be flawed
const int *cip = &ci; // ok
*cip = 7; // syntax error
ip = cip; // syntax error, C++ keeps constness
cip = ip; // ok, but now:
*cip = 5; // syntax error, wherever cip points is const
int const *icp; // same as const int *
icp = &i; // ok, can assign to, icp is NOT const, *icp IS
*icp = 5; // syntax error, wherever icp points is const
/* Can a pointer be constant? */
int * const ipc = &i; // ipc IS const, must be initialized
*ipc = 5; // OK, where ipc points to is NOT a const
int * const ipc2 = &ci; // syntax error, ipc2 is NOT pointer to const
const int * const cipc = &ci; // const pointer to a const
/* Use constants to detect errors */
int my_strlen(char *s)
{
char *p = s;
// Here the programmer has made a fatal error
while ( ! (*p = 0) ) ++p;
return p - s;
}
// This program likely cause run-time error
int main()
{
char t[] = "Hello";
std::cout << my_strlen(t) << std::endl; // t converted to const char *
return 0;
}
// This is the correct declaration of the parameter
int my_strlen(const char *s)
{
// Which requires const char* here:
const char *p = s;
// prog.cpp: In function 'int my_strlen(const char*)':
// prog.cpp:11:19: error: assignment of read-only location '* p'
while ( ! (*p = 0) ) ++p;
return p - s;
}
// Declaration of 3 arrays in the user data area, read and write permissions for the elements:
char t1[] = {'H','e','l','l','o','\0'};
char t2[] = "Hello";
char t3[] = "Hello";
// Declaration of 2 pointers in the user data area, read and write permissions for the pointers
// and allocation of the "Hello" literal (possibly) read-only
char *s1 = "Hello"; // s1 points to 'H'
char *s2 = "Hello"; // s2 likely points to the same place
void *v1 = t1, *v2 = t2, *v3 = t3, *v4 = s1, *v5 = s2;
std::cout << v1 << '\t' << v2 << '\t' << v3 << '\t' << v4 << '\t' << v5 <<std::endl;
// the result (v1, v2 v3 are different, v4 and v5 could be the same):
// 0x23fe10 0x23fe00 0x23fdf0 0x404030 0x404030
// assignment to array elements:
*t1 = 'a'; *t2 = 'b'; *t3 = 'c';
// modifying string literal: could be segmentation error:
*s1 = 'd'; *s2 = 'e';
// The type of "Hello" is const char[6].
// const char[] --> char* conversion is only for C reverse compatibility
char *s3 = "Hello"; // warning: deprecated conversion from string constant to 'char*'
const char *s4 = "Hello"; // correct way, no write permission through the pointer
*s4 = 'f'; // syntax error, const-correctness is not flawed
const int c1 = 1;
const int c2 = 2;
const int c3 = f(3);
const int *p = &c2;
int t1[c1];
int t2[c2];
// prog.cpp:18:12: warning: ISO C++ forbids variable length array 't3'
int t3[c3];
int i; std::cin >> i;
switch(i)
{
case c1: std::cout << "c1"; break;
case c2: std::cout << "c2"; break;
// prog.cpp:25:10: error: 'c3' cannot appear in a constant-expression
case c3: std::cout << "c3"; break;
}
If the compiler knows every use of a const, it does not need to allocate space to hold it.
// no memory needed
// need memory
// need memory
class Date
{
public:
Date(int year, int month, int day);
int getYear();
int getMonth();
int getDay();
void setDay(int year, int month, int day);
private:
int year;
int month;
int day;
};
Can we assure whether a method will not modify the state of the object?
Date today(2015,06,22);
const Date birthday(1990,2,16);
birthday = today; // syntax error: birthday is const
birthday.setYear(2000,1,1); // syntax error, can we assure, setYear will not modify?
int year = birthday.getYear(); // but can we assure, getYear will not modify?
class Date
{
public:
Date(int year, int month, int day);
int getYear() const;
int getMonth() const;
int getDay() const;
void setDay(int year, int month, int day);
private:
int year;
int month;
int day;
};
We can mark a method constant, expressing that it does not affect the state of the class:
const Date birthday(1990,2,16);
birthday.setYear(2000,1,1); // syntax error
int year = birthday.getYear(); // ok
template <typename T, ... >
class vector
{
public:
T& operator[](size_t i);
const T& operator[](size_t i) const;
};
What about constant data members?
We can overload on constness of a member function:
vector<int> v = { 10, 20, 30, 40, 50 }; // since C++11
const vector<int> cv = v;
int& x = v[1]; // will call the non-const overload
const int& y = cv[1]; // will call the const overload
v[1] = 25; // ok
cv[1] = 25; // syntax error, cv[1] is const int&
class Msg
{
public:
Msg(const char *t);
int getId();
private:
const int id;
std::string txt;
};
Msg m1("first"), m2("second"); // parameter is not the id
if(m1.getId() != m2.getId())
{
/* ... */
}
MSg::Msg(const char *t)
{
txt = t;
id = getNewId(); // syntax error, id is const
}
Msg::Msg(const char *t) : id(getNextId()), txt(t) // use initialization list
{ }
struct Point
{
public:
void getXY(double& x, double& y) const;
void setXY(double x, double y);
private:
double xcoord;
double ycoord;
mutable int m;
};
const Point a;
a.m = 10; // ok, m is mutable
A mutable member does not affect the externally visible state of the class, hence mutable members of const objects can be modified.
What can be a real use case scenario for mutable members?
struct Point
{
public:
void getXY(double& x, double& y) const;
void setXY(double x, double y);
private:
double xcoord;
double ycoord;
mutable std::mutex m;
};
void Point::getXY(double& x, double& y) const
{
std::lock_guard<std::mutex> lg(m);
x = xcoord;
y = ycoord;
}
void Point::setXY(double x, double y)
{
std::lock_guard<std::mutex> lg(m);
xcoord = x;
ycoord = y;
}
void printPoint(const Point &p)
{
double x, y;
p.getXY(x, y);
std::cout << "X: " << x
<< ", Y: " << y << std::endl;
}
void increasePoint(Point &p)
{
double x, y;
p.getXY(x, y);
p.setXY(x * 2, y * 2);
}
int main()
{
Point p;
p.setXY(1.1, -1.1);
std::thread t1(printPoint, std::ref(p));
std::thread t2(increasePoint, std::ref(p));
std::thread t3(increasePoint, std::ref(p));
std::thread t4(printPoint, std::ref(p));
t1.join(); t2.join(); t3.join(); t4.join();
return 0;
}
int f(int i)
{
return i;
}
class X
{
static const int sci1 = 7; // ok, but remember definition
static int si = 8; // error: not const
const int ci = 9; // error: not static (acceptable since C++11)
static const int sci2 = f(2); // error: initializer not const
static const float scf = 3.14; // error: not integral
};
const int X::sci1 = 7; // do not repeat initializer here...
int X::si = 8; // would be ok
int i = 6; // i is not declared const
const int& ci = i;
const_cast<int&>(ci) = 42; // OK: modifies i
std::cout << "i = " << i << std::endl; // OK, will be 42
const int j = 6; // j is declared const
int* pj = const_cast<int*>(&j);
*pj = 42; // undefined behavior!
std::cout << "j = " << j << std::endl; // can be both 6 or 42
Among all the castings, const_cast is the only one that may be used to cast away (remove) constness or volatility.
What about the classic C-stlye casting?
int* pj = (int*) j;
Note: dynamic_cast is never applied
wrapper w;
w.modify(42);
std::cout << "w.data = " << w.data << std::endl;
const wrapper cw;
cw.modify(42); // undefined behavior!
std::cout << "cw.data = " << cw.data << std::endl; // can be both 6 or 42
void (wrapper::*mp)(int) const = &wrapper::modify; // pointer to member function
const_cast<void(wrapper::*)(int)>(mp); // compiler error: const_cast does not work
// on function pointers
Similar case with more complex types:
struct wrapper
{
wrapper() : data(6) { }
void modify(int value) const
{
// this->data = value; // compile error: this is a pointer to const
const_cast<wrapper*>(this)->data = value; // OK as long as the type object isn't const
}
int data;
};
// STL is const-safe, e.g.:
template <typename It, typename T>
It find(It begin, It end, const T& t)
{
while (begin != end)
{
if ( *begin == t )
{
return begin;
}
++begin;
}
return end;
}
// With plain pointers:
const char t[] = { 1, 2, 3, 4, 5 };
const char *p = std::find(t, t+sizeof(t), 3)
if(p)
{
*p = 6; // syntax error
}
// With iterators:
const std::vector<int> v(t, t+sizeof(t));
std::vector<int>::const_iterator i =
std::find(v.begin(), v.end(), 3);
if(v.end() != i)
{
*i = 6; // syntax error
}
STL in const-safe, e.g.:
std::vector<int> v1(4,5);
auto i = std::find(v1.begin(), v1.end(), 3); // i is vector::iterator
const std::vector<int> v2(4,5);
auto j = std::find(v2.begin(), v2.end(), 3); // j is vector::const_iterator
std::vector<int> v3(4,5);
auto k = std::find(v3.cbegin(), v3.cend(), 3); // k is vector::const_iterator (since C++11)
// Issue in C++98:
std::vector<int> v(3,5);
std::vector<int>::const_iterator ci = std::find( v.begin(), v.end(), 3); // implicit casting
v.insert(ci, 2); // syntax error in C++98, not in C++11 (since g++ 4.9.2)
// C++11: use global begin() and end() for greater generality
std::vector<int> v1(4,5);
auto i = std::find(begin(v1), end(v1), 3); // i is vector::iterator
const std::vector<int> v2(4,5);
auto j = std::find(begin(v2), end(v2), 3); // j is vector::const_iterator
std::vector<int> v3(4,5);
auto k = std::find(cbegin(v3), cend(v3), 3); // not in C++11, only since C++14
int t[4] = { 1, 2, 3, 4 };
auto l = std::find(std::begin(t), std::end(t), 3); // l is int*
A constant expression is an expression that is possible to be evaluated at compile time. Such expressions can be used as non-type template arguments, array sizes, and in other contexts that require constant expressions.
Beside core constant expressions one can define more generalized constant expressions with the constexpr keyword since C++11.
Constexpr variable:
Constexpr function:
C++11: body must be either deleted or defaulted or contain only the following:
C++14: body must be either deleted or defaulted or contain any statements except:
Constexpr constructor:
parameters must be of literal types
must have no virtual base classes
the constructor must not have a function-try block
base class and every non-static member must be initialized
every implicit conversion involved must be a constant expression
body must satisfy similar criteria as for contexpr functions
Constexpr functions can be called with all constexpr parameter values. The return value can be either assigned to a constexpr or to a non-constexpr variable.
/* strlen as constant expression */
constexpr int my_strlen(const char *s)
{
const char *p = s;
while ('\0' != *p) ++p;
return p-s;
}
// C++11:
// prog.cpp: In function 'constexpr int my_strlen(const char*)':
// prog.cpp:9:1: error: body of constexpr function 'constexpr int my_strlen(const char*)'
// not a return-statement
int main()
{
std::cout << "String length: " << my_strlen("Hello World!") << std::endl;
return 0;
}
C++11: constexpr must be a single return statement
C++14: fine
// C++11
constexpr int my_pow(int base, int exp) {
return exp == 0 ? 1 : base * my_pow(base, exp-1);
}
// C++14
constexpr int my_pow(int base, int exp) {
auto result = 1;
for (int i = 0; i < exp; ++i) result *= base;
return result;
}
// Output function that requires a compile-time constant, for testing
template<int n> struct constN {
constN() { std::cout << n << std::endl; }
};
int main() {
int b = 4; // could be user input
std::cout << "2^10 = "; constN<my_pow(2, 10)> out1; // compile time: guaranteed
std::cout << "3^10 = " << my_pow(3, 10) << std::endl; // compile time: for optimization
std::cout << b << "^10 = " << my_pow(b, 10) << std::endl; // computed at runtime
// prog.cpp:32:51: error: the value of 'b' is not usable in a constant expression
// test.cpp:25:7: note: 'int b' is not const
std::cout << b << "^10 = "; constN<my_pow(b, 10)> out2;
return 0;
}
// C++11
constexpr const float pi = 3.14f; // const is not enough!
constexpr float degree2radian(float degree)
{
return degree * pi / 180.0f;
}
int main()
{
std::cout << degree2radian(60.0f) << std::endl;
return 0;
}