Advanced C++

Constants

Máté Cserép

October 2015, Budapest

Overview

  • Constant correctness (values, pointers, references)
  • Whey and why to use constants?
  • Character arrays and string literals
  • When constants allocate memory?
  • Constant member functions
  • Constant and mutable members
  • Constant casting (const_cast)
  • Constant correctness in STL
  • Constant expressions (constexpr)

Constant correctness: values & references

/* 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

Constant correctness: pointers

/* 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

When and why to use constants?

  • Use constants to express that a value should not be changed
     
  • Use constants to let the compiler detect programming errors
     
  • Use constants to enable certain optimalizations
     
  • Use of constants is required in some cases (e.g. array size, template argument,
    enum declaration)

Detecting errors with constants

/* 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;
}

Character arrays and string literals

// 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

When constants allocate memory?

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

Constant member functions

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?

Constant member functions

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

Constant member functions

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&

Constant data members

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
{ }

Mutable data members

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?

Mutable data members: mutexes

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;
}

Static constant members

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

Can a programmer bypass const correctness?

What could be the consequences?

Constant casting

      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?

  1. const_cast
  2. static_cast
  3. static_cast followed by const_cast
  4. reinterpret_cast
  5. reinterpret_cast followed by const_cast
int* pj = (int*) j;

Note: dynamic_cast is never applied

Constant casting

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;
};

Constant correctness in STL

// 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.:

Begin and end iterators in STL

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*

Constant expressions

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:

  • immediately constructed or assigned
  • must contain only literal values, constexpr variables and functions
  • the constructor used must be constexpr constructor

Constant expressions

Constexpr function:

  • must not be virtual
  • return type must be a literal type
  • parameters must be of literal types

C++11: body must be either deleted or defaulted or contain only the following:

  • null statements
  • static_assert declarations
  • typedef declarations and alias declarations that do not define classes or enumerations
  • using declarations
  • using directives
  • exactly one return statement

C++14: body must be either deleted or defaulted or contain any statements except:

  • asm declarations
  • goto statements
  • try-blocks
  • definition of non-literal type variables
  • static or thread storaged variable definitions
  • uninitialized variables

Constant expressions

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.

Example: string length

/* 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

Example: mathematical functions

// 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;
}

Example: mathematical functions

// 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;
}

Advanced C++: Constants

By Cserép Máté

Advanced C++: Constants

  • 318