Friday, January 10, 2014

Chapter:05 Static Assert


Fundamental
=========



Static assert concept is like compile time equivalent to "assert()".  In past we all have been using assert() in different places in our code bases. "assert()" is simple yet very powerful tool which gives many useful information when you are running the debug version of your executable."assert()" gets such functionality by calling "abort()" which ultimately terminate the program at that time. By this way we normally get all useful information close to the root cause of the problem. By this way we can fix all such problem and then in the release version of our executable, we can remove "assert()" or we can define the NDEBUG macro by which "assert()" call gets ignored under the release version.



Now just imagine, wouldn't it be beautiful if  similar functionality is achieved at compile time rather than run time. Static assert concept has been introduced to achieve this functionality. Obviously there would be some limitation if some functionality works in compile time, as many important things are not known at compile time else the world would have been so nice and beautiful.


Static assert only evaluates the expression which happens to be the constant expression(constexpr/const) type. This limitation does not hold true for "assert()" functionality. However while learning this concept, I found that actually this limitation is not bad, but it sort of forces  good programming practices to be applied. I find that many times we define our variables  as literals or static/global variable or local variable type which never gets changed in program execution. Mostly we can do the same thing using 'constexpr/const' if we do slight change in our way of thinking. I know these lines might look like a bit philosophical, however, at this point, I can only share what I felt over the period of learning these concepts. Hopefully readers would realize the same in near future.



Uses
====


Now lets write some basic program using static assert concept. The below program actually examines the size of integer data with a constexpr variable. We all know that size of integer or any data type may vary depending on various machine  architectures and whether machine is 32 or 64 bit type. And suppose that you have written your program logic depending on the size of integer value, you may want to validate that logic during the compilation time of your program rather than gets impacted in the form of some nasty run time error. In such circumstances, static assert would be very helpful and the below program actually does the same thing.



//Code Begins
#include<iostream>

void learn_static_assert(void)
{   
 /* change it to 4,so that program can compile */
 constexpr int intsize = 8;
 static_assert(intsize <=sizeof(int),"integers are too small");
 int x{0};
 x = intsize*intsize;
 std::cout<<x<<std::endl;
}

int main(int argc, const char* argv[])
{
 learn_static_assert();
 return 0;
}

//Code Ends



When I compile the above program, I get the following output which is expected on my machine where size of integer is 4 byte. However inside static assert function, there is a check whether size of integer is less than 8 byte. This will not hold true under out circumstances. So the program will print the static assert message and abort the compilation of the program.





//Console Output Begins

$ g++ -g -gdwarf-2 -std=c++11 -Wall test_1.cpp -o test_1
test_1.cpp: In function ‘void learn_static_assert()’:
test_1.cpp:7:2: error: static assertion failed: integers are too small
  static_assert(intsize <=sizeof(int), "integers are too small");
  ^

//Console Output Ends



To illustrate that static assert does not play any role in the run time,  lets change the above program so that it can compile successfully. If we change the 'intsize' to 4 or less, which was causing the static assert to fail, this program would compile. Now we run/debug this program under "gdb",we can see by ourself that whether static assert line actually gets executed or not. When I did 'step' command to the static assert line "gdb" goes to next line instead of doing anything on static assert line. I also looked the "disassemble" command result and did not find any such instruction in the assembly code which shows any static assert piece of  instruction is there. This confirms that, this does not have any role in the run  time of program.



//Debugger Output Begins

(gdb) break main
(gdb) run
Breakpoint 1, main (argc=1, argv=0x7fffffffe1f8) at test_1.cpp:16
16        learn_static_assert();
(gdb) step
learn_static_assert () at test_1.cpp:6
6        constexpr int intsize = 4;
(gdb) disassemble learn_static_assert
Dump of assembler code for function learn_static_assert():
   0x0000000000400820 <+0>:      push   %rbp
   0x0000000000400821 <+1>:      mov    %rsp,%rbp
   0x0000000000400824 <+4>:      sub    $0x10,%rsp
=> 0x0000000000400828 <+8>:   movl   $0x4,-0x8(%rbp)
   0x000000000040082f <+15>:    movl   $0x0,-0x4(%rbp)
   0x0000000000400836 <+22>:    movl   $0x10,-0x4(%rbp)
   0x000000000040083d <+29>:    mov    -0x4(%rbp),%eax
   0x0000000000400840 <+32>:    mov    %eax,%esi
   0x0000000000400842 <+34>:    mov    $0x601060,%edi
   0x0000000000400847 <+39>:    callq  0x4006a0 <_ZNSolsEi@plt>
   0x000000000040084c <+44>:    mov    $0x400700,%esi
   0x0000000000400851 <+49>:    mov    %rax,%rdi
   0x0000000000400854 <+52>:    callq  0x4006f0 <_ZNSolsEPFRSoS_E@plt>
   0x0000000000400859 <+57>:    leaveq
   0x000000000040085a <+58>:    retq  
End of assembler dump.
(gdb) step
8        int x{0};
(gdb) n
9        x = intsize*intsize;
(gdb) c
Continuing.
16

//Debugger Output Ends




The real use of static assert is in the generic programming concept. We  all know that how difficult it is to understand the compilation error message when we use the STL or any other template based program. To improve this functionality for template based program, "concept" is planned to introduced in C++14.This hopefully would be great for us and after this we need not worry for these nasty compilation error messages. I think real bad compilation error is one of the main reason why everyone in C++ community prefer not to use template based programs as they should have been by now. This is serious problem over the years, and hence Bjarne Stroustrup and ISO C++ standard committee had proposed the "concept" solution for these problems.


However at present we are in C++11 world and it would take some time when "concept" would be introduced and understood by us. For now we can think  as "concept" are like predicates which would work in compile time. I think "concept" would or must  use static assert and type traits functionality in its implementation, which is available at present in the language. The examples discussed in this chapter are rather simple. Think how you can apply these language feature to implement type checking feature for template based programs in real sophisticated scenario. These are not so easy to learn and apply. At least I found it bit difficult and it took some time for me to understood it even partially.



The below program checks whether argument type is  float for the function "f()". If  by some means somewhere some code actually tries to pass the non-float type data to the "f()", static assert would abort the compilation. This is great because if due to some mistake, if your program would be doing the implicit type conversion while passing argument to "f()", it would get caught at compile time.
It's great and that too it does it in the compile time.



//Code Begins
#include<type_traits>
#include<iostream>

template<typename T>
constexpr bool Is_floating_point() {
 return std::is_floating_point<T>::value;
}

template<typename T>
void  f(T& a)  {
 static_assert(Is_floating_point<T>(), "Non-Float Type Data");
}

void learn_static_assert_and_typetraits(void) {
 float y{10.0};
 f(y);
 float x{100};
 f(x);

 float sum{0.0};
 sum=x+y;
 std::cout<<sum<<std::endl;
}

int main(int argc, const char* argv[]) {
 learn_static_assert_and_typetraits();
 return 0;
}

//Code Ends




When I compile the above program, I get the following compilation error when someone tries to pass the argument which happens to be "int" type.


//Console Output Begins

$ g++ -g -gdwarf-2 -std=c++11 -Wall test_2.cpp -o test_2
test_2.cpp: In instantiation of ‘void f(T&) [with T = int]’:
test_2.cpp:20:6:   required from here
test_2.cpp:12:3: error: static assertion failed: Non-Float Type  Data
   static_assert(Is_floating_point<T>(), "Non-Float Type  Data");
   ^
}
//Console Output Ends



"malloc()" is very important function in C/C++ program. However we all know that this function can create all sorts of problem if you are not aware of some of its internal concepts. The argument type of "malloc()" is of 'size t' type which is typedef of some unsigned. Now if our program passes the argument to "malloc()" by 'signed int' variable and if it becomes some negative value due to some reason, we are in big big trouble. Because under this situation, system would interpret your 'signed in' variable to 'unsigned' which would be very very large value. Due to this, "malloc()"would return NULL to its caller.



At many obvious places in our program we normally do not check the return address by "malloc()" even though it should be done as good programming practice. This would lead to program crash at the point depending on how you have handled/used this returned memory in code. This is nasty and not so easy bug to identify in real complex system.


Just to make "malloc()" more type safe, I have written the below program. I would not say that this is the ideal/complete solution for the problem which I have discussed above. Nevertheless it shows the reader how we can use these simple static asserts and type traits language mechanism in efficient way in our program.



//Code Begins

#include<type_traits>
#include<cstdlib>
#include<cstring>
#include<cstdio>
#include<iostream>

template<typename T>
void display(const T& val) {
 std::cout<<val<<std::endl;
}

template<typename T>
constexpr bool Is_unsigned_number() {
 return std::is_unsigned<T>::value;
}

template<typename T>
void  check_unsigned(T& a)  {
 static_assert(Is_unsigned_number<T>(), "Non Unsigned Type  Data Found");
}

template<typename T>
void* my_malloc(T val) {
 /* Comment Out The Below check and see how program crash */
 check_unsigned(val);
 return malloc(val);
}


void learn_static_assert_and_typetraits(void) {
 constexpr std::size_t sz1{10};
 void* p_a=my_malloc(sz1);
 *(int *)p_a=sz1;

 constexpr int sz2{-1};
 void* p_b=my_malloc(sz2);
 *(int *)p_b=sz2;
}

int main(int argc, const char* argv[]) {
 learn_static_assert_and_typetraits();
 return 0;
}

//Code Ends




When I compile the above program, I get the following compilation error where program tries to pass the 'signed int' variable.


//Console Output Begins

$ g++ -g -gdwarf-2 -std=c++11 -Wall test_4.cpp -o test_4
test_4.cpp: In instantiation of ‘void check_unsigned(T&) [with T = int]’:
test_4.cpp:26:21:   required from ‘void* my_malloc(T) [with T = int]’
test_4.cpp:38:26:   required from here
test_4.cpp:20:3: error: static assertion failed: Non Unsigned Type  Data Found
static_assert(Is_unsigned_number<T>(), "Non Unsigned Type  Data Found");
^

//Console Output Ends




If we comment out the function which actually does the argument type checking before the actual “malloc()” call we can verify that program would crash at run time. This is because we have not done NULL check. But from my experience I can tell you that this is not so non obvious situation in real code. You can verify by yourself,by commenting that piece of code as mentioned in the code comment as well. I guess by now reader would slowly be realizing the power and beauty of these simple concepts.



//Debugger Output Begins

(gdb) break main
(gdb) run

Breakpoint 1, main (argc=1, argv=0x7fffffffe1f8)
44            learn_static_assert_and_typetraits();
(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x0000000000400751 in learn_static_assert_and_typetraits ()
39    *(int *)p_b = sz2;
(gdb) bt
#0  0x0000000000400751 in learn_static_assert_and_typetraits ()
#1  0x000000000040076d in main (argc=1, argv=0x7fffffffe1f8)
(gdb) p p_b
$1 = (void *) 0x0
(gdb) info locals
sz1 = 10
p_a = 0x602010
sz2 = -1
p_b = 0x0

//Debugger Output Ends



The below program explains how we can check whether a particular class is polymorphic type or not. There are many such type traits functions introduced in C++11. Please read those functions and think how you can use those in your program depending on your requirement. 


In the current example, I have written one class which has more than 'virtual' methods. Clearly, this is of polymorphic type.  The other class in the program is 'std::vector'. This is not of polymorphic type and it should not be implemented as polymorphic by any vendor. These simple check can be further used whether we create the object on free store or not. Because it does not make much sense to create the object on free store of the class which is of 'concrete' type and not 'polymorphic' type. Hope it make some sense to reader what I am trying to convey.


//Code Begins

#include<type_traits>
#include<iostream>
#include<string>
#include<vector>

template<typename T>
void display(const T& val) {
 std::cout<<val<<std::endl;
}

class base
{
public:
 base(std::string n): name(n) {}
 base(): base{"base"} {}
 virtual std::string get_name(void) { display(name); }
 virtual ~base() { }   
private:
 std::string name;
};

template<typename T>
constexpr bool Is_valid_poly() {
 return std::is_polymorphic<T>::value;
}

template<typename T>
void  check_polymorphic(T& a)  {
 static_assert(Is_valid_poly<T>(), "Its Not Polymorphic Class");
}


void learn_static_assert_and_typetraits(void) {
 base obj;
 check_polymorphic(obj);   
               
 std::vector<int> obj2;
 check_polymorphic(obj2);
}

int main(int argc, const char* argv[]) {
 learn_static_assert_and_typetraits();
 return 0;
}

//Code Ends




When I compile the above program,I get the following compilation error.


//Console Output Begins

$ g++ -g -gdwarf-2 -std=c++11 -Wall test_5.cpp -o test_5
test_5.cpp: In member function ‘virtual std::string base::get_name()’:
test_5.cpp:17:55: warning: no return statement in function
returning non-void [-Wreturn-type]
   virtual std::string get_name(void) { display(name); }
                                                       ^
test_5.cpp: In instantiation of ‘void check_polymorphic(T&)
[with T = std::vector<int>]’:
test_5.cpp:39:25:   required from here
test_5.cpp:30:3: error: static assertion failed:
Its Not Polymorphic Class
static_assert(Is_valid_poly<T>(), "Its Not Polymorphic Class");
 ^

//Console Output Ends

No comments:

Post a Comment