Immutable programming in C++
Zoltán Porkoláb
gsd@elte.hu
Eötvös Loránd University
Budapest, Hungary
Agenda
- Pure functions
- Const in C++
- Const correctness in C++
- Constexpr in C++11 and 14
- Lambdas and const
Constants in C++
================
Design goals of C++
- Type safety
C++ is a statically strongly typed programming language
The compiler decides the type of all (sub)expression in compile time
In run time: pointers, conversions, Object-oriented constructs
- Resource safety
Not just memory! All resources (files, sockets, locks, etc.)
No garbage collection by def. (but can implement)
Use the RAII (Resource Acquisition Is Initialization) idiom
Most beginners make resource errors
- Performance
Direct access to HW resources. No virtual machine
High performance trading, phone exchange systems
Low energy cosumption (Mars rover)
- Predictability
Large systems (2-10 million eLoC)
Orthogonal features should work well together
- Learnability, readability
C++11: from expert-friendly to novice-friendly
C++ is rich to express semantical concepts by syntax
#include <stdio.h>
int main()
{
FILE *fp = fopen( "input.txt", "r");
fprintf( fp, "%s\n", "Hello input!");
fclose(fp);
return 0;
}
$ gcc -std=c99 -pedantic -Wall -W wrong.c
$ ./a.out
Segmentation violation
#include <fstream>
int main()
{
std::ifstream f;
f << "Hello input!" << std::endl;
return 0;
}
$ g++ -std=c++0x wrong.cpp
wrong.cpp: In function ‘int main()’:
wrong.cpp:10:8: error: no match for ‘operator<<’ in ‘f << "Hello input!"’
wrong.cpp:10:8: note: candidates are:
/usr/include/c++/4.6/ostream:581:5: note: template<class _CharT, class _Traits, class _Tp> std::basic_ostream<_CharT, _Traits>& std::operator<<(std::basic_ostream<_CharT, _Traits>&&, const _Tp&)
/usr/include/c++/4.6/ostream:528:5: note: template<class _Traits> std::basic_ostream<char, _Traits>& std::operator<<(std::basic_ostream<char, _Traits>&, const unsigned char*)
/usr/include/c++/4.6/ostream:523:5: note: template<class _Traits> std::basic_ostream<char, _Traits>& std::operator<<(std::basic_ostream<char, _Traits>&, const signed char*)
/usr/include/c++/4.6/ostream:510:5: note: template<class _Traits> std::basic_ostream<char, _Traits>& std::operator<<(std::basic_ostream<char, _Traits>&, const char*)
/usr/include/c++/4.6/bits/ostream.tcc:323:5: note: template<class _CharT, class _Traits> std::basic_ostream<_CharT, _Traits>& std::operator<<(std::basic_ostream<_CharT, _Traits>&, const char*)
/usr/include/c++/4.6/ostream:493:5: note: template<class _CharT, class _Traits> std::basic_ostream<_CharT, _Traits>& std::operator<<(std::basic_ostream<_CharT, _Traits>&, const _CharT*)
/usr/include/c++/4.6/ostream:473:5: note: template<class _Traits> std::basic_ostream<char, _Traits>& std::operator<<(std::basic_ostream<char, _Traits>&, unsigned char)
/usr/include/c++/4.6/ostream:468:5: note: template<class _Traits> std::basic_ostream<char, _Traits>& std::operator<<(std::basic_ostream<char, _Traits>&, signed char)
/usr/include/c++/4.6/ostream:462:5: note: template<class _Traits> std::basic_ostream<char, _Traits>& std::operator<<(std::basic_ostream<char, _Traits>&, char)
/usr/include/c++/4.6/ostream:456:5: note: template<class _CharT, class _Traits> std::basic_ostream<_CharT, _Traits>& std::operator<<(std::basic_ostream<_CharT, _Traits>&, char)
/usr/include/c++/4.6/ostream:451:5: note: template<class _CharT, class _Traits> std::basic_ostream<_CharT, _Traits>& std::operator<<(std::basic_ostream<_CharT, _Traits>&, _CharT)
/usr/include/c++/4.6/bits/basic_string.h:2693:5: note: template<class _CharT, class _Traits, class _Alloc> std::basic_ostream<_CharT, _Traits>& std::operator<<(std::basic_ostream<_CharT, _Traits>&, const std::basic_string<_CharT, _Traits, _Alloc>&)
Constant correctness
#include <iostream>
#include <iomanip>
int lower = -100;
int upper = 400;
int step = 40;
int main()
{
for( int fahr = lower; upper >= fahr; fahr += step )
{
std::cout << "fahr = " << std::setw(4) << fahr
<< ", cels = " << std::fixed << std::setw(7)
<< std::setprecision(2) << 5./9. * (fahr-32)
<< std::endl;
}
return 0;
}
$ g++ -std=c++0x fahr.cpp
$ ./a.out
fahr = -100, cels = -73.33
fahr = -60, cels = -51.11
fahr = -20, cels = -28.89
fahr = 20, cels = -6.67
fahr = 60, cels = 15.56
fahr = 100, cels = 37.78
fahr = 140, cels = 60.00
fahr = 180, cels = 82.22
fahr = 220, cels = 104.44
fahr = 260, cels = 126.67
fahr = 300, cels = 148.89
fahr = 340, cels = 171.11
fahr = 380, cels = 193.33
But only a small modification in the code ....
#include <iostream>
#include <iomanip>
int lower = -100;
int upper = 400;
int step = 40;
int main()
{
for( int fahr = lower; upper = fahr; fahr += step )
{
std::cout << "fahr = " << std::setw(4) << fahr
<< ", cels = " << std::fixed << std::setw(7)
<< std::setprecision(2) << 5./9. * (fahr-32)
<< std::endl;
}
return 0;
}
$ g++ -std=c++0x fahr.cpp
$ ./a.out
...may result infinite loop
But, if we use constants
#include <iostream>
#include <iomanip>
const int lower = -100;
const int upper = 400;
const int step = 40;
int main()
{
for( int fahr = lower; upper = fahr; fahr += step )
{
std::cout << "fahr = " << std::setw(4) << fahr
<< ", cels = " << std::fixed << std::setw(7)
<< std::setprecision(2) << 5./9. * (fahr-32)
<< std::endl;
}
return 0;
}
$ g++ -std=c++0x fahr.cpp
fahr.cpp: In function ‘int main()’:
fahr.cpp:10:34: error: assignment of read-only variable ‘upper’
Use the compilation flags for warnings!
$ g++ -std=c++0x -pedantic -Wall -W fahr.cpp
fahr.cpp: In function ‘int main()’:
fahr.cpp:10:34: error: assignment of read-only variable ‘upper’
fahr.cpp:10:34: warning: suggest parentheses around assignment used as truth value [-Wparentheses]
What kind of constants are in C++
Preprocessor macros
- Replaced by literals
#include <iostream>
#include <iomanip>
#define LOWER -100
#define UPPER 400
#define STEP 40
int main()
{
for( int fahr = LOWER; UPPER = fahr; fahr += STEP )
{
std::cout << "fahr = " << std::setw(4) << fahr
<< ", cels = " << std::fixed << std::setw(7)
<< std::setprecision(2) << 5./9. * (fahr-32)
<< std::endl;
}
return 0;
}
$ g++ -std=c++0x fahr.cpp
fahr.cpp: In function ‘int main()’:
fahr.cpp:10:34: error: lvalue required as left operand of assignment
Disadvantages
- No scope
- No lifetime
- No type (the replacing literal has type)
String literals
- Values are known at compile-time:
const char *hello1 = "Hello world";
char *hello2 = "Other hello";
hello1[1] = 'a';
hello2[1] = 'a';
char *s = const_cast<char *>(hello1);
s[3] = 'x';
This kind of data stored outside of the program writeable area.
Therefore write attempts are undefined.
There is difference between a string literal and an initialized character
array.
#include <iostream>
int main()
{
char t1[] = {'H','e','l','l','o','\0'};
char t2[] = "Hello";
char t3[] = "Hello";
char *s1 = "Hello";
char *s2 = "Hello";
void *v1 = t1, *v2 = t2, *v3 = t3, *v4 = s1, *v5 = s2;
std::cout <<v1<<'\t'<<v2<<'\t'<<v3<<'\t'<<v4<<'\t'<<v5<<std::endl;
0xbffff460 0xbffff450 0xbffff440 0x8048844 0x8048844
*t1 = 'x'; *t2 = 'q'; *ct = 'y';
*s1 = 'w'; *s2 = 'z';
return 0;
}
Named constants
Named constants have special linkage rules
- const has no linkage - it is "local" to source file
- extern const has linkage
- we have to distinguish declaration and definition
const int ic = 10; const int ic = 20;
extern const int ec = 30; extern const int ec;
They know more what you expected
#include <iostream>
int f(int i) { return i; }
int main()
{
const int c1 = 1;
const int c2 = 2;
const int c3 = f(3);
int t1[c1];
int t2[c2];
int i; std::cin >> i;
switch(i)
{
case c1: std::cout << "c1"; break;
case c2: std::cout << "c2"; break;
}
return 0;
}
- Since C++11 we can use constexpr functions - see later
Does a const requires to allocate memory?
:
#include <iostream>
int f(int i) { return i; }
int main()
{
const int c1 = 1;
const int c2 = 2;
const int c3 = f(3);
const int *p = &c2;
int t1[c1];
int t2[c2];
int t3[c2];
return 0;
}
The compiler have to allocate memory
- when the const is initialized at run-time
- when there exists at least one pointer pointing to that const
Proving
#include <iostream>
int f(int i) { return i; }
int main()
{
int mark1;
const int c1 = 1;
const int c2 = 2;
const int c3 = f(3);
int mark2;
size_t m1 = reinterpret_cast<size_t>(&mark1);
size_t m2 = reinterpret_cast<size_t>(&mark2);
std::cout << m1 << std::endl;
std::cout << m2 << std::endl;
std::cout << "distance = " << ( m1>m2 ? m1-m2 : m2-m1 ) << std::endl;
return 0;
}
$ g++ -std=c++0x -pedantic -Wall -W memory.cpp
$ ./a.out
140734134410892
140734134410888
distance = 4
#include <iostream>
int f(int i) { return i; }
int main()
{
int mark1;
const int c1 = 1;
const int c2 = 2;
const int c3 = f(3);
int mark2;
const int *ptr = &c2;
size_t m1 = reinterpret_cast<size_t>(&mark1);
size_t m2 = reinterpret_cast<size_t>(&mark2);
std::cout << m1 << std::endl;
std::cout << m2 << std::endl;
std::cout << "distance = " << ( m1>m2 ? m1-m2 : m2-m1 ) << std::endl;
return 0;
}
$ g++ -std=c++0x -pedantic -Wall -W memory.cpp
$ ./a.out
140734135699932
140734135699924
distance = 8
But be care with optimizations!
#include <iostream>
int main()
{
const int ci = 10;
int *ip = const_cast<int*>(&ci);
++*ip;
cout << ci << " " << *ip << endl;
return 0;
}
Constant correctness
====================
Motivation
- Suppose we try to implement strlen() ...
- ... and we make mistakes
#include <iostream>
int my_strlen(char *s)
{
char *p = s;
while ( ! (*p = 0) ) ++p;
return p - s;
}
int main()
{
char t[] = "Hello";
std::cout << my_strlen(t) << std::endl;
return 0;
}
$ g++ -std=c++0x -pedantic -Wall -W strlen.cpp
$ ./a.out
Segmentation violation
$
Try to do it in a constant correct way
- Since strlen() don't want to modify the string declare it to const
- const char * is a "pointer to constant"
- Not the pointer is the constant - but the pointed memory
#include <iostream>
int my_strlen(const char *s)
{
const char *p = s;
while ( ! (*p = 0) ) ++p;
return p - s;
}
int main()
{
char t[] = "Hello";
std::cout << my_strlen(t) << std::endl;
return 0;
}
$ g++ -std=c++0x -pedantic -Wall -W strlen.cpp
strlen.cpp: In function ‘int my_strlen(const char*)’:
strlen.cpp:6:21: error: assignment of read-only location ‘* p’
$
This reveals the mistakes we made, and we can fix it,
but this requires all affected variables to declare
as pointer to const.
This is the correct version:
#include <iostream>
int my_strlen(const char *s)
{
const char *p = s;
while ( ! (*p == 0) ) ++p;
return p - s;
}
int main()
{
char t[] = "Hello";
std::cout << my_strlen(t) << std::endl;
return 0;
}
A side note about defensive programming
#include <iostream>
int my_strlen(char *s)
{
char *p = s;
while ( ! (0 == *p) ) ++p;
return p - s;
}
const correctness and pointers
==============================
int i = 4;
i = 5;
const int ci = 6;
ci = 7;
int *ip;
ip = &i;
*ip = 5;
Now lets try a trick
ip = &ci;
*ip = 7;
ip = &ci;
const int *cip = &ci;
*cip = 7;
ip = cip;
cip = ip;
*cip = 5;
int const *icp;
icp = &i;
*icp = 5;
Can a pointer to be const?
:
int * const ipc = &i;
*ipc = 5;
int * const ipc2 = &ci;
const int * const cccp = &ci;
What about the user defined types?
:
class Date
{
public:
Date( int year, int month = 1, int day = 1);
int getYear();
int getMonth();
int getDay();
void set(int y, int m, int d);
private:
int year;
int month;
int day;
};
const Date my_birthday(1963,11,11);
Date curr_date(2015,7,10);
my_birthday = curr_date;
... but ...
int year = my_birthday.getYear();
my_birthday.set(2015,7,10);
class Date
{
public:
Date( int year = 2000, int month = 1, int day = 1);
int getYear() const;
int getMonth() const;
int getDay() const;
void set(int y, int m, int d);
private:
int year;
int month;
int day;
};
const Date my_birthday(1963,11,11);
Date curr_date(2015,7,10);
int year = my_birthday.getYear();
int year = curr_date.getDay();
curr_date.set(2015,7,11);
my_birthday.set(2015,7,10);
const and mutable members
Const members are immutable under the object lifetime
class Msg
{
public:
Msg(const char *t);
int getId() const { return id; }
private:
const int id;
std::string txt;
};
Msg m1("first"), m2("second");
m1.getId() != m2.getId();
MSg::Msg(const char *t)
{
txt = t;
id = getNewId();
}
MSg::Msg(const char *t) : id(getNextId()), txt(t)
{
}
Mutable members can be changed even from const member functions
struct Point
{
void getXY(int& x, int& y) const;
double xcoord;
double ycoord;
mutable int read_cnt;
};
const Point a;
++a.read_cnt;
#include <mutex>
struct Point
{
public:
void getXY(int& x, int& y) const;
private:
double xcoord;
double ycoord;
mutable std::mutex m;
};
void getXY(int& x, int& y) const
{
std::lock_guard< std::mutex > guard(m);
x = xcoord;
y = ycoord;
}
Static const are like non-member const but in class namespace.
class X
{
static const int c1 = 7;
static int i2 = 8;
const int c3 = 9;
static const int c4 = f(2);
static const float f = 3.14;
};
const int X::c1;
Overloading on immutability
template <typename T, ... >
class std::vector
{
public:
T& operator[](size_t i);
const T& operator[](size_t i) const;
};
int main()
{
std::vector<int> iv;
const std::vector<int> civ;
iv[i] = 42;
int i = iv[5];
int j = civ[5]
}
The Standard Template Library is const-safe
===========================================
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;
}
const char t[] = { 1, 2, 3, 4, 5 };
const char *p = std::find( t, t+sizeof(t), 3)
if ( p )
{
std::cout << *p;
}
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 )
{
std::cout << *i;
}
std::vector<int> v1(4,5);
auto i = std::find( v1.begin(), v1.end(), 3);
const std::vector<int> v2(4,5);
auto j = std::find( v2.begin(), v2.end(), 3);
auto k = std::find( v1.cbegin(), v1.cend(), 3);
std::vector<int> v(3,5);
std::vector<int>::const_iterator ci = std::find( v.begin(), v.end(), 3);
v.insert(ci, 2);
std::vector<int> v(3,5);
auto ci = std::find( v.cbegin(), v.cend(), 3);
v.insert(ci, 2);
std::vector<int> v(4,5);
auto i = std::find( begin(v), end(v), 3);
std::vector<int> v(4,5);
auto i = std::find( cbegin(v), cend(v), 3);
std::vector<int> v(4,5);
auto i = std::find( cbegin(v), cend(v), 3);
Constexpr
=========
Constexpr in C++11
Constexpr objects
const objects having value that is known at translation time.
translation time = compilation time + linking time
they may have placed to ROM
- immediately constructed or assigned
- must contain only literal values, constexpr variables and functions
- the constructor used must be constexpr constructor
Constexpr function
can produce constexpr values when called with compile-time constants.
- must not be virtual
- return type must be literal type
- parameters must be literal type
Literal types:
- all build-in types (except void)
- user-defined types with constexpr constructor
- C++11:
Only contain a single expression (return statement)
constexpr non-static memberfunctions are implicitly const
- C++14:
declaring variable that is not static or local_thread
if / else / switch
for / ranged-for / while / do-while
objects with lifetime began in constexpr can mutate
removes the rule that constexpr non-static memberfunctions are implicitly const
additional restrictions for static and local_thread variables
- cannot contain
asm definition
goto
try block
Constexpr constructor
- parameters must be literal type
- must have no virtual base classes
- either deleted or defaulted or contain only the following:
...as above...
- 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
Samples (from Scott Meyers: Effective Modern C++)
constexpr int pow( int base, int exp) noexcept
{
return exp == 0 ? 1 : base * pow(base, exp-1) ;
}
constexpr int pow( int base, int exp) noexcept
{
auto result = 1;
for (int i = 0; i < exp; ++i) result *= base;
return result;
}
class Point
{
public:
constexpr Point(double xVal = 0, double yVal = 0) noexcept
: x(xVal), y(yVal) {}
constexpr double xValue() const noexcept { return x; }
constexpr double yValue() const noexcept { return y; }
void setX(double newX) noexcept { x = newX; }
void setY(double newY) noexcept { y = newY; }
private:
double x, y;
};
constexpr Point p1(42.0, -33.33);
constexpr Point p2(25.0, 33.3);
constexpr Point midpoint(const Point& p1, const Point& p2) noexcept
{
return { (p1.xValue() + p2.xValue()) / 2,
(p1.yValue() + p2.yValue()) / 2 };
}
constexpr auto mid = midpoint(p1, p2);
constexpr Point reflection(const Point& p) noexcept
{
Point result;
result.setX(-p.xValue());
result.setY(-p.yValue());
return result;
}
constexpr auto midReflected = reflection(mid);
Template metaprograms only emulates floating point vales.
#include <iostream>
constexpr int cstrlen(const char *s)
{
const char *p = s;
while ( '\0' != *p ) ++p;
return p-s;
}
int main()
{
constexpr int len = cstrlen("Hello");
std::cout << len << std::endl;
return 0;
}
$ g++ -std=c++11 -pedantic -Wall -W conststrlen.cpp
conststrlen.cpp: In function ‘constexpr int strlen(const char*)’:
conststrlen.cpp:8:1: error: body of constexpr function ‘constexpr int strlen(const char*)’ not a return-statement
$ g++ -std=c++14 -pedantic -Wall -W conststrlen.cpp
$ ./a.out
5
$
Avoiding side effects
constexpr int def_size(int n)
{
static int value = n;
return value;
}
int x;
struct A
{
constexpr A(bool b) : m(b?42:x) { }
int m;
};
constexpr int v = A(true).m;
constexpr int w = A(false).m;
constexpr int f1(int k)
{
constexpr int x = k;
return x;
}
constexpr int f2(int k)
{
int x = k;
return x;
}
constexpr int incr(int &n)
{
return ++n;
}
constexpr int g(int k)
{
constexpr int x = incr(k);
return x;
}
constexpr int h(int k)
{
int x = incr(k);
return x;
}
constexpr int y = h(1);
The non-template metaprogram factorial function
constexpr auto factorial(unsigned n)
{
unsigned long long res = 1;
while ( n > 1) res *= n--;
return res;
}
int main()
{
static_assert(120 == factorial(5), "loop version incorrect");
return 0;
}
Lambdas and constness
=====================
- C++11: lambda functions
- C++14: generalized lambdas
int main()
{
vector<int> v;
for(int i = 0; i < 10; ++i)
v.push_back(i);
for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });
cout << endl;
return 0;
}
$ g++ l.cpp
$ ./a.out
0 1 2 3 4 5 6 7 8 9
$
[] lambda-introducer
(int n) lambda parameter declaration
{ ... } compound statement
Lambda expressons define classes and construct objects.
The program above is equivalent to:
struct LambdaFunctor
{
void operator() (int n) const { cout << n << " "; }
};
int main()
{
vector<int> v;
for(int i = 0; i < 10; ++i)
v.push_back(i);
for_each(v.begin(), v.end(), LambdaFunctor());
cout << endl;
return 0;
}
Definitions from Meyers' Effective Modern C++:
Lambda expression: [](int n) { cout << n << " "; }
Closure: runtime object created from lambda. May hold captured variables.
Closure class: the type of the closure object.
Capture by value
int main()
{
vector<int> v;
for(int i = 0; i < 10; ++i)
v.push_back(i);
int x = 0;
int y = 0;
cin >> x >> y;
v.erase( remove_if(v.begin(),
v.end(),
[x,y](int n) { return x < n && n < y; }
),
v.end()
);
for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });
cout << endl;
return 0;
}
3 6
0 1 2 3 6 7 8 9
We passed the x and y parameters by value. Basically it is equivalent:
struct LambdaFunctor
{
public:
LambdaFunctor(int a, int b) : m_a(a), m_b(b) { }
bool operator()(int n) const { return m_a < n && n < m_b; }
private:
int m_a;
int m_b;
};
v.erase( remove_if(v.begin(),v.end(),LambdaFunctor(x,y)), v.end());
The x and y parameters are copied and being stored in the function object.
We cannot modify the captured values because the operator() in functor is
const. It is a "real" copy, therefore the modification of x and y is not
reflected inside the lambda.
auto f = [=](int n) { auto sz = v.size(); return x < n && n < y; };
cout << "size of f = " << sizeof(f) << endl;
size of f = 32
[=] default-capture lambda introducer. Captures all locals by value.
Lambda expressions are const by default but we can manage to modify x and y.
Note, that modification of prev does not update local prev variable.
#include <algorithm>
#include <iostream>
#include <ostream>
#include <vector>
using namespace std;
int main()
{
vector<int> v;
for(int i = 0; i < 10; ++i)
v.push_back(i);
int prev = 0;
for_each(v.begin(), v.end(), [=](int& r) mutable {
const int oldr = r;
r *= prev;
prev = oldr;
});
for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });
cout << "prev = " << prev << endl;
return 0;
}
0 0 2 6 12 20 30 42 56 72 prev = 0
Capture by reference
#include <algorithm>
#include <iostream>
#include <ostream>
#include <vector>
using namespace std;
int main()
{
vector<int> v;
for(int i = 0; i < 10; ++i)
v.push_back(i);
int prev = 0;
for_each(v.begin(), v.end(), [&prev](int& r) {
const int oldr = r;
r *= prev;
prev = oldr;
});
for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });
cout << "prev = " << prev << endl;
return 0;
}
0 0 2 6 12 20 30 42 56 72 prev = 9
[&] Capture everything by reference
[=, &x, &y] Capture everything by value but x and y which capured by reference
[&, x, y] Capture everything by reference but x and y which capured by value
References
==========
Bjarne Stroustrup: The C++ Programming Language (4th Edition)
Addison-Wesley Professional; 4 edition (May 19, 2013) ISBN-10 0321563840
Bartosz Milewski: Functional Data Structures in C++
(C++Now, Aspen, CO, US, 2015)
https:
Scott Meyers: Effective modern C++ (O'Reilly Media, 2014)
ISBN 978-1-4919-0399-5 | ISBN 10 1-4919-0399-6
Peter Sommerlad: C++14 Compile-time computation (ACCU 2015)
http: