Published on

Types and Declration

Overview

This discussion has been laid down into the following sections:

C++ ISO Standards

The C++ Programming Language for the most of the part generally confirms to its standards laid down by International Standard Organization, therefore in the places where the text mentioned in this disussion could be considered imprecise, incomplete or possibly wrong, you are free to consult the standards, however, don't expect the stadard to be the text easily accessible by non-expert. Moreover, strictly adhering to the standards doesn't by itself garuntees good code or even portable at that, In fact, it's quite possible even for a vetran programmer to write a perfectly awful standard-confirming code. Consequently most of the programmer generally finds their leeway around the standards they do so to access system interface, and feature that can't be expressed directly into C++ or require relience on certain implemenation specific details.

A construct is deemed implemenation defined, if for that construct you have a specified, well-defined behavior and that behavior is often well-documented. For example:

#include<iostream>
using namespace std;
void implentation_Defined(){
  char ch = 12;
  cout << ((int)ch == 12) << endl;
  char ch1 = 256;
  int check = 256;
  cout << ((int)ch1 == (check >> 8)-1) << endl;
}
int main(){
  use_implementation_Defined();
}

In the above example the intialization of ch is well-defined cause on any given implemenation a character is garunteed to occupy atleast a byte meaning it hold 256 different value ranging either from -127 to 127(no not from -127 to 128 as one might expect as C++ leave an a bit for the open possiblity to represent the signed bit for the one's compliments) as in the signed characters, or 0 to 255 as in the unsigned characters. But the intialization of ch1 is implementation defined cause the no of bits used to represent the character when extended to all bit one pattern in integers is implementation-defined. Ohter behavior can be considered unspecified, i.e. a range of the behavior is possible for such construct but the implementer is not obliged to specify which of the following might actuall occur for instance, the value returend from new and assigned to a thread are generally unspeicifed unless some synchronization mechanisms has been implemented to prevent data races.

While writing real world programs it is usually imperative to relies on these implementation-defiend behavior, such behavior is the price we pay to operate effectively on a larger ranger of system. For instance, C++ would've been much more simpler if all of the character would've been 8 bits or if all the pointer occupies 32 bits, however, there are implementation in widespread use for which a character might be represented in 32bits, or pointer in 64 bits depending on the range of computation or precision required by such implementation.

To maximize portability it is thus, advisible to be explicit about such implementation-defined properties, and to isolate more subtle example in clearly marked section of the program. A typical example of this practise would be to present all the hardware size dependencies in the form of constant or type definition in some header files. Many of such assumptions can also be checked by using numeric_limits and employing assertions.

#include<iostream>
#include<math.h>
using namespace std;
void check_assumptions(){
  int min_int1 = std::numeric_limits<int>::min();
  int min_int2 = pow(2, 31) * -1;
  int max_int1 = std::numeric_limits<int>::max();
  int max_int2 = pow(2, 31)-1;
  static_assert(sizeof(int) <= 4, "sizeof int is too small");
  //Thus for any given type T occupying n bits the range of compuation for that type can be determined as -2^n/2 to 2^n/2-1 or -2^n-1 to 2^n-1-1 for signed representation and 0 to 2^n-1-1 for unsinged representation.
}
int main(){
  check_assumptions();
}

Undefined behaviors are considered nastier, a behavior is deemed undefined if no reasonable is required by the implementation. Usually the obvious implementation techniques will cause the programs to behave badly when using undefined behavior. For instance:

#include<iostream>
using namespace std;
void undefined_behavior(){
  const int sizes = 12;
  int arr[sizes]{};
  arr[sizes + sizes] =13;
  cout << arr[sizes + sizes];
}
int main(){
  undefined_behavior();
}

The possible outcome of the above code might involves overwriting unrelated memory location for the sequence of objects of same time in the memory, thereby leading to a possible hardware exception or error. In the presence of powerful optimizers however, the exact outcome for such undefined behavior could become quite obscure and unpredictable. Usually if a plausible and easily implementable alternative exist, a feature should be deemed implementation-defined or unspecified rather than undefined. It's thus worth spending considerable amount of time to ensure that a program doesn't use something that is deemed undefined by the implementation or standard and in many cases tools exist to help us do exactly that.

Implementations

The C++ implementation can either be hosted or freestanding. A hosted implementation generally provided all the facilites as required by the standards, a free standing however, might provides fewer facilities as long as the bare minminal set of feature required by a compiler to run a given C++ programs are provided. As such, a free-standing implementation is generally meant for code running on a system with minimal operating system support. Many such implementatin, however, can also be extended to provide support for exceptions amongst other things.

Free standing Implementation Headers

Implementation PropertyImplementation Header
Types<cstddef>
Excetpion<exception>
Other implementation properties<cstdint>, <cstdbool>, <cstdarg>
Integer Types<cstdint>
start and termination<cstdlib>
Dynamic Memory Management<new>
Type traits<type_traits>
Initializer_list<intializer_list>
Type identification<typeinfo>
Atomics<atomic>

The Basic Source Character sets

Most of the C++ programs and the examples in this discussion are written in the basic source characterset consisting of letters, digits, punctuation, some graphical characters, whitespace, etc., that corresponds to the US variants of the 7-bit characterset called ASCII, as supported by the given implementation of the language. However, since such charactersets are generally only partially standarized this can use problems for people using C++ in an enviorment with different character symbol, though we can still extends the basic character set to other character set, for instance by using universal character literals, etc., however, programming in multi-lingual enviorment is beyound the scope of the given discussion, even though it has be preluded at several places. In general though it is advisible to not to make assumption about the representation of character much like any given object, however in some case it still garunteed that:

  • An enviorment using ASCII characterset will provide support for punctuation characters, operator symbols, such as }, !, |, etc., that are not available in some character set.
  • We need a notion to represent characters that don't have any convinient representation on keyboard such as the whitespace characters, newline line, tabs, etc.
  • ASCII doesn't contains characters that are used for writing in language other than English.

Types

C++ Types corresponds to the most basic storage units of the computer and the way to use them to hold onto the representation of the data. As such before any object or identifier could be used in C++, it must be suitibly declared, i.e. it must be associated with the particular type corresponding to the amount of the storage, and hence range of compuation, precision, etc., required to hold onto the representation of such identifer. for istance:

int f(int);
void type_declrations(){
  int x = 12;
  int r = f(x) + x;
}

The above-mentioned example makes sense cause f is declared to be a function that takes in an integer and returns and integer, hence it can be assigned to an integral type, and can be operated upon with arthematic operators. The given blog discuss at length the corresponding C++ types and declration, as such the example presented here, though aren't that meaninful in real world but can be significantly elevated to better comprehend C++, and hence write it effectively, or more so importantly to understand the code written by other in a better manner. As such, if you feel overwhelmed by the details mentioned here, you might want to skim through the given blog, and return later to it as and when the need for the same arises.

Fudamental Types

C++ has a set of fundamental types that corrresponds to the most basic storage unit of the computers and the ways to use them to hold onto the representation of data. As such, C++ fundamental types could be considered as the primitive types from which other types can be formulated. Rougly speaking C++ fundamental type generally includes the following:

  • Integral Types: int(short, long, long long, int, etc), char(char32_t, char16_t, wchar_t, char16_t, etc), short, bool, etc., are often collectively known as the integral type meaning they can be implicitly converted to integeral value in arthematic expressions, etc., and hence can also be freely assigned to each other. Integral type in general as their name suggest are often used to represnt integral whole values.
  • Floating Point types: float, double, long, long double, etc., collectively consitites the floating point types, as such floating points types are generally used to represent approximation of real number in fixed amount of memory.
  • Arthematic Types: Integral types and floating point types are collectively known as the Arthematic types.
  • A type void, use to signify the absence of information or return type.
  • From these types we can formaulate other types using the declrator operator. such types are often known as the built-in types, and can usually includes, the pointer for the given type such as (int*, float*, double*, etc), arrays(int[2], int[], float[3], double[2][3], etc.), References(int&, int&&, const int&, etc).
  • The C++ fundamental types together with the pointer and array provides a machine level notion in a rather implementation independent manner, mostly owning to the fact even though different fundamental type might have some implementation-specific properties such as the memory allocate to represent each of such type may vary on different implementation, yet the way we access and store the object or identifier associated with the particular type always employs the same addressing or referencing mechanisms.
  • Still, the integral and floating point type may comes in variety of size to given programmer the flexibility in amount of storage consumed, and hence the range, precision availbale for computation on the objects of the corresponding types. Usually the assumption regarding the same to a certain extend might go around with the a computer providing a byte of storage(8 bits) to hold and represent character(in that sense characters could be considered as the smallest adressable entity in C++), a word for integers and an entity most suitable for floating point numbers, and addresses for reffering those entities.

Booleans

A Boolean type is generally used when one need to check some predicate or as a return type for a logical expression. Consequently, it can assume only two value true or false. And though, it may sound obscure since, the boolean literal are techically integral literals, they generally converts to the integral value in the arthematic expression where true corresponds to 1 and false to 0, conversly speaking, an integral literal can be implicitly converted to bool where, a non-zero value converts to true and a zero value converts to false. Moreover, since the boolean value converts to integral value in arthematic operation and such operation would be applied on the converted values. For intance: A pointer can also be converted to a boolean value where a nullptr corresponds to false, and a non-null pointer corresponds to true. This fact can also be exploited to facilitates short hand evaluation or arthematic operations where the second operand is not evaluated based on the conditional value of the first operand of an arthematic operator.

#include<iostream>
using namespace std;
void Booleans(){
  bool a = true;
  bool b = false;
  bool c = 12;
  bool d = 0;
  bool f{12!=0}; //if you want to use list-intialization to prevent narrowing, yet still want to conver integral literal value to bool you can be explicit about it.
  cout << a << " " << b << " " << c << " " << d << endl;
  cout << (a + b) << " " << (b - a) << " " << (c * d) << " " << (d /a) << endl;
  cout << (a && b) << " " << (b || c) << " " << (!d) << " " << endl;
  cout << (a & b) << " " << (b | c) << " " << (c ^d) << " " << (~d) << endl;
  cout << (a &= b) << " " << (b |= d) << " " << (c ^= d) << endl;
  cout << (a and_eq b) << " " << (b or_eq d) << " " << (c xor_eq d) << endl;
}

bool is_open(FILE* fp){
  const int bufsize = 1024;
  char buffer[bufsize];
  if(fp){
    fread(buffer, sizeof(buffer), 1, fp);
  }else{
      return false;
  }
  const char* somestring = buffer;
  int count{};
  while(somestring && *somestring++){
    if(count == 0){cout << *(somestring-1);count = 1;} //not efficient but whatever :)
    cout << *somestring;
  }
  return true;
}
void use_is_open(){
  FILE* fp = fopen("something.txt", "r");
  is_open(fp);
}
int main(){
  Booleans();
  use_is_open();
}

Character Types

There are many character sets and character set encoding in use. Consequently, C++ has a variety of character types that often represnt a bewildering variety to accords for all of the different character sets as supported by the implementation locale.

  • char: default character type used for program text. A char is used for implementation's character set and is usually 8 bits meaning it can account for 256 different value, usually the value appearing on your keyboard.
  • signed char: Like char types but garunteed to be signed.
  • unsinged char: like char types but garunteed to be unsinged. Usually unsinged literal type doesn't need to take care of the signed bit for the one's compliment of negitive representation of the given character, and hence have more range than their signed counterparts. However, using unsinged literal with that intent is almost always garunteed to backfire mostly owing to subtle confusion arising from unecessary conversions.
  • wchar_t: A character type that is provided to hold larger character sets such as the unicode. Though the range supported by wchar_t is usually implementation specify but is large enough to hold the largest character as supported by the implementation locale.
  • char32_t: A char type for holding exactly 32 bits characters.
  • char16_t: A type for holding exactly 16 bits characters.

These are six distinct type despite of the fact that _t is often used to denote aliases. Thus on any given implementation char will be identical to either signed char or unsinged, but these three names are still considered different, consequently we can though mix char literals freely but we can't assign pointers to different char types to each other pertaining to the fact that each of the signed, plain and unsinged char literal might have different range of compuation, could be aligned at different word bit boundary, and hence using a pointer to other char literal type that wasn't assigned that way could lead to subtle problems especially in terms of efficiency while storing and accessing such literals.

For most of the intent and purposes it is safe to assume that the character literal:

On any given implementation a character could be either signed or unsinged, however, if not specified otherwise, it is usually implementation defined wheter the plain character turns out to be signed or unsinged. Consequently, one might tend to argue to use only the specific character literal type as opposed to plain character, however, there is a huge plathero of backward compability issue from the native C code implementation that can reek into causing a multitude of issue while working with the said approach, in particular, most of the standard library function such as strcmp that relies on the plain representation of character literals to work.

Almost uninversally a character has 8 bits and can hold the character literal enclosed with the '' as supported by the implementation's characterset. However, since most of the charactersets such as the ASCII are only partially stadarized, special consideration is needed when using them to support different natural langauges. In that breathe it is safe to assume that the ASCII characterset supports the decimal digits, the 26 alphabetical characters of English, and some other basic punctuation, graphical character, however, it is not safe to assume that:

  • There is no more than 127 characters in a 8 bit characterset, Most of these characterset might provide more characters for instance an unsinged 8 bit character literal can assume 255 different values.
  • All of the characters implemented with the ASCII characters set are contiguous, for instnace, EBCDIC leaves a gap between i and j.
  • A character fits in 1 bytes, there are implementation where a character might take more or less than a byte particularly on a memory contrived enviorment such as an embedded system.
  • Every character used in C++ are available. There are some national character set that don't provide |, {, }, [,], etc.

Even though we have laid down some assumptions about the character set, whenever possible we should avoid making assumptions about the representation of an object and the same applies even to the characters.

Each character has an integer value associated with it which represent it in the given character set. For instnace, in ASCII characterset the character literal 'a' corresponds to the integral value 97. Consequently, the notion of converting the source characterset into the integral value gives us the ability to represent each of such character in its equivalent binary representation as well. To print the integral representation of all of the character as supported by the given implementation local one can use the following code snippest.

However, the notion of coverting character literal values into integral value astoundingly arise the question wheter the character are signed or unsinged. And as disscussed above it is implementation defined wheter the character turns out to be signed or unsigned on a given implementation.

Since character types are integral the arthematic, and bitwise logica operations applies to them. For instance:

#include<iostream>
using namespace std;
void use_ASCII(){
  for(char ch; cin >>ch;){
    cout << static_cast<int>(ch) << endl;
    if(ch == 'z'){break;}
  }
  for(size_t i = 0; i < 26; i++){
    cout << static_cast<char>('a' + i) << " ";  //way to print characters from 'a' to 'z'
  }
}
enum class OPS{AND, OR, XOR, AND_EQ, OR_EQ, XOR_EQ, MUL, DIV};
void do_operations(OPS o1, char ch, int i){
  switch(o1){
    case OPS::AND: cout << (ch & i) << endl; break;
    case OPS::OR: cout << (ch | i) << endl; break;
    case OPS::XOR : cout << (ch ^ i) << endl; break;
    case OPS::AND_EQ: cout << (ch &= i) << endl; break;
    case OPS::OR_EQ: cout << (ch |= i) << endl; break;
    case OPS::XOR_EQ: cout << (ch ^= i) << endl; break;
    case OPS::MUL: cout << (ch * i) << endl; break;
    case OPS::DIV: cout << (ch / i) << endl; break;
  }
}
void use_do_operations(){
  for(char ch = 'a'; ch <= 'z'; ch++){
    do_operations(OPS::AND, ch, 1);
    do_operations(OPS::OR, ch, 2);
    do_operations(OPS::XOR, ch, 3);
    do_operations(OPS::AND_EQ, ch, 4);
    do_operations(OPS::OR_EQ, ch, 5);
    do_operations(OPS::XOR_EQ, ch, 6);
    do_operations(OPS::MUL, ch, 7);
    do_operations(OPS::DIV, ch, 8);
  }
}
int main(){
  use_ASCII();
  use_do_operations();
}

Signed and Unsigned Character literals

As discussed earlier, a plain character literal can be either signed or unsigned, but these theree are still distinct type, consequently, even though you can freely assign amongst different character types, you can't mix pointer to differnt character type cause they might be aligned at different word bit bound, might occupy different space in memory, etc. But even though character literals can be freely assigned amongst integral literals, assigning too large of a value to a signed literal can lead to unfortunate conversion to fit in the given assigned value within the permissible range for signed characters. None of these potential problem, confusion occur if one tend to stick to only using character literals for storing character vlaue, and avoid assigning integral values to them, in particular negitive character values.

#include<iostream>
using namespace std;
void freely_mix_character_literals(char ch, signed char sc, unsigned char uc){
  ch = 255; //implementation defined if ch is signed.
  cout << (int)ch << endl;
  ch = sc; //OK
  cout << (int)ch << endl;
  ch = uc; //implementation defined if ch is signed and uc is too large.
  cout <<(int)ch << endl;
  sc = ch; //OK
  cout << (int)sc << endl;
  sc = uc; //implementation defined if uc is too large.
  cout << (int)sc << endl;
  uc = ch; //OK conversion to unsigned.
  cout << (int)uc << endl;
  uc = sc; //OK conversion to unsigned.
  cout << (int)uc << endl;
}

void cant_mix_character_pointer(char* ch, signed char* sc, unsigned char* uc){
    char ch1 = 'a';
    signed char ch2 = 'b';
    unsigned char ch3 = 255;
    ch = &ch1; //OK
    sc = &sc1; //OK
    uc = &uc1; //OK
    cout << *ch << " " << *sc << " " << *uc << endl;
    //ch = &sc1; //Error cant convert signed char* to char*
    //ch = &uc1; //Error cant convert unsigned char* to char*
    //sc = &ch1; //Error cant convert char* to signed char*
    //sc = &uc1; //Error cant covert unsigned char* to signed char*
    //uc = &ch1; //Error cant covert char* to unsigned char*
    //uc = &sc1; //Error cant covert signed char* to unsigned char*
}
int main(){
  freely_mix_character_literals('a', -12, 255);
  char a = 'a';
  signed char b = 'b';
  unsigned char c = 'd';
  cant_mix_character_pointer(a, b, c);
}

Character literals

A character literal is represented by a single character enclose within singe quotes, for example 'a', '0', etc. The type of such character literal is of couse, char. A character literal can be implicitly converted to the integral value corresponding to the character set on which the C++ proram is to run. This sort of use of character literals rather than the decinal notiation makes the program more portable. However, there are a few characters within the characterset that don't have any convinient representation on the keyboard for intance the newline line character, to represent such special character we often need to escape them with the escape character i.e. ''. Some of these character with their corresponding escape notation are as mentioned below:

NameASCII NameC++ Name(Escape Sequence)
newlineNL\n
Horizontal tabHT\t
Vertical TabVT\v
Single Quote''
Double Quotes""
carrige returnCR\r
form feedFF\f
Question Mark??
AlertBEL\a
BackSpaceBS\b
octal Numbersooo\ooo
Hexadecimal Numbersxxx\xxx
Backslash\\\

Despite their appearance these are single character, we can often represent the implementation characterset as a one, two or three digiti octal, or hexadecimal number followed by the corresponding escape sqeuence. This in turn makes it possible to represent every characters in the machine characterest. There is infact no limits to the hexadecimal or octal digits in such sequence, and they are considered to break at the point of first such digit that not a octal or hexadecimal number, respectively. This notion of representing character set as a hexadecimal or octal number is imparitive because using numberic notation for characters makes it non-portable across machines with different characterset As such we can also embed more than one character in a character literals for example the literal 'ab', but such use of character literals is often rightfully considered archic and implemnetation defined. The type of such character literals.

When embedding numeric constants in the string using the octal or the hexadecimal notation it is wise to be explicit about the number of digits in such sequences and always use three digits to represent a number as such notion is already hard enough to understand without having to worry about the next digit in sequence being a constant or a character.

Despite of the widespread use of the 7 bit ASCII character set, they are limited in the range of the character they can represent. As such, we can employ a much richer character such as the universal character set at our disposal as and when the need for the same arises. Usually such character are used in UTF string literals and the meaning of the character embeeded within such sequence for the most of the part is implementation defined. These character can be represented either as 4(UTF8) or 8 digit hexadecimal literals, using a hexadecimal digits different from four or eight to represent sequences however, can results in lexical errors.

#include<iostream>
#include<math.h>
#include<map>
#include <string>
using namespace std;
map<int, char>m1{{10, 'a'}, {11, 'b'}, {12, 'c'}, {13, 'd'}, {14, 'e'}, {15, 'f'}};
string revstring(string s){
  int start = 0, end = s.size()-1;
  while(start <= end){
    char a = s[start];
    char b = s[end];
    s[start] = b;
    s[end] = a;
    start++; end--;
  }
  return s;
}
string DecToHexa(int r){
  string res = "";
  int count{};
  while(r >= 0){
    if(res == "" && r == 0){return "0"; }
    if(r == 0){break;}
    if(r%16  < 10){res += to_string(r%16);}
    else{
      res += m1[r%16];
    }
    r = r/16;
  }
  return revstring(res);
}
int hexToDecimal(string hexa){
  hexa = revstring(hexa);
  int res = 0;
  int count{0};
  for(auto& i: hexa){
    if(hexa.size() == 1 && i == '0'){return 0;}
    if(i >= '0' && i < '9'){res += pow(16, count) * (i - '0'); count += 1;}
    else{
      for(map<int, char>::iterator it = m1.begin(); it != m1.end(); it++){
        if(it->second == i){
          res += it->first * pow(16, count);
          count += 1;
        }
      }
    }
  }
  return res;
}
void use_universal_character_litearls(){
  const char32_t* utf_32 = U"\x0000DEAD";
  const char16_t* utf_16 = u"\xDEAD";
  cout << *utf_32 << endl;
  cout << *utf_16 << endl;
}
int main(){
  string somestring{"random"};
  string somestring1[somestring.size()]{};
  string res = "";
  for(size_t i = 0; i < somestring.size(); i++){
    somestring1[i] = DecToHexa((int)somestring[i]);
    cout << somestring1[i] << " ";
    char hexanumber = hexToDecimal(somestring1[i]);
  }
  //72 61 6e 64 6f 6d
  char randomstrings[]{"\x72\x61\x6e\x64\x6f\x6d"};
  cout << randomstrings << endl;
  use_universal_character_litearls();
  return 0;
}

Integer Types

Like char integers are used to represents the whole representation of a numbers(either signed or unsigned) and much like chars, they also comes in three guises i.e. plain integers, signed integers, and unsigned integers. In addition to that, integers also comes in four different size to cater to the user needs of computation at least in terms of range and precission available for a given integeral representation. Correspondingly an integer could be either short int, plain int, long int, long long int, etc. A long int in colloqial sense can also be reffered just as long, simliarly a short int can also be refferd just as short. No there is no long short int counterpart to an integers!

Unsinged integers as in character can occupy one more extra bit as they might not need to tend to the signed bit for the one's compliment representation, however, they should best be used when one need to treat storage as a bit array, Attempts to ensure that some value are positive by using unsinged integers are almost always bound to be defeated by implicit conversions. Unlike plain character however, a plain integers is usually garunteed to be singed, in that sense, except for the extended range, one can almost use the plain integers interchanbily with their signed int couterparts.

If one want more of extended control over the size of the integer type being used on a given implementation, one can be explicit about such integer, in fact, one of the standard library implementation's header called <cstddef> provides us with a whole host of integral types such as int32_t(an integer occupying exactly 32 bits), int16_t(an integer occupying exactly 16 bits), int64_t(an integer occupying exactly 64 bits), fast_int16_t(an integer with exactly 16 bits, supposedly the fastest such integer), etc., with the corresponding sizes.

Integer literals

Integer literals comes in three guises the decimal, octal and hexadecimal. Each of the corresponding integer literals follows the same semantic as using them in character literals, however, they should also be used to treate storage as a bit array cause using such literals to represent genuine number can lead to surprises. For instnace the hexadecimal integeral literal 0xffff would have the corresponding value of 65535 on implementation with 32 bit integeral literals, however had less bits have been used to represent the integer literal the corresponding assignment of an integer to such literal value could lead to possible truncation. Moreover, in addition to explicitly specying the type of the integral value one can append a prefix to indicate the corresponding integeral values being storaged in accordance to the provided type, for instnace, using a suffix l, explicitly indicates the user's intent to storage the corresponding integral value as long even if the value could be accounted for in an otherwise, smaller size integral types, etc. Simliarly using a suffix u specifies the intent for an unsigned integral value, one can also blend in the two prefix to arrive at the intented type for istance using ul could be considered equivalent to the long unsinged integral types.

#include<math.h>
#include<iostream>
using namespace std;
void use_integer_types_and_literals(){
  int plain_int = 12;
  cout << sizeof(plain_int) << endl;
  signed int signedint = -12;
  cout << sizeof(signedint) << endl;
  unsigned int unsingedint = pow(2, 32)-1;
  cout << unsignedint << endl;

  int octal = 012;
  cout << (octal == 10 ? "True" : "False") << endl;

  int hexadecimal = 0xff;
  cout << (hexadecimal == (17 * 15) ? "True" :"False") << endl;

  int16_t truncation = 0xffff;
  cout << (truncation == -1 ? "True" : "False") << endl;

  int32_t hexforbitpattern = 0xffff;
  cout << (hexforbitpattern == 65535 ? "True" : "False") << endl;
}
int main(){
  use_integer_types_and_literals();
  return 0;
}

Floating Point Types

The floating point types represents floating points numbers. The floating points number can be roughly defined as the approximation of real numbers in a fininte amount of memory. Consequently, floating points numbers often find their extensive use in the arthematic operations and compuation, in particular operations that requires precision for instance optimizing some sort of cost function often requires extended presions for floating points. Consequently C++ offers three flavors of such floating points numbers, that can be used and exploited depending upon the range of the compuation and hence the precision required by the representation of such numbers. While the plain floats could be used effectively for the single precison compuation the docuble, and longs offers double and extended precision respectively. However, no matter how intuative the single points, double and extended precision may sound their exact meaning usually remains the question of debate and requires significant understanding of floating point compuation. Consequenlty, if one fall short in such understanding, one should take a step back and take some time to learn while delving deeper into the requisite details, or just use doubles and hope for the best.

Floating Point literals

By default the floating point literals are double, again the compiler ought to warn you if the literal is too large to be represented within the permissible range of the specified type. A floating point literal can also be defined in its equivalent scientific noation resembling the tradtional mathematical notation of such numbers with the requsitie metisa and exponents parts where the metissa of couse, indicate the base number and the exponent along with the mentioned sign indicate the power of ten to which it should be multipied to result in the intented value. For instance a number 1.2e-2 indicates a floating point number 0.012, while the number 1.2e+2 is equivalent to a floating point number 120.0. Moreover, much like the integral literals one can explicitly specify the floating point literal type by prefixing the value for thus intialized object of the requsitie type with the intended prefixes such as the f, l, etc. One must also keep into the consideration that despite their appearance the entirety of the exponential sequence for the representation of floating point number should be treated as the a single lexical token as opposed to separate one. Consequently, writing the exponent and metisa parts of such notation differently could result in the evident parsing error while generating the parsed tree for the thus produced token stream or Symbol table form the given source.

#include<iostream>
#include<limits>
using namespace std;
void use_floating_types(){
  float singleprecision = std::numeric_limits<float>::max();
  cout << singleprecision << endl;
  double doubleprecision = std::numeric_limits<float>::max() + 12;
  cout << doubleprecision << endl;
  long double extendedprecison = std::numberic_limits<double>::max() + 12;
  cout << extendedprecison << endl;

  double exponential = 1.2e-12f;
  cout << exponential << endl;
}
int main(){
  use_floating_types();
}

void

The type void syntactically can be considered as a fundamental type. It can however, be used only as part of a more complicated type; there are not object of type void, in particular a void type is used either to specify that a function doesn't return a value or as a base type to a pointer to object of unkonwn type or void* pointer. For example:


#include<iostream>
using namespace std;
int main(){
  //void x; //error there are no object of type void.
  //void& x; //error there are no references of type void.
  void* r = reinterpret_cast<void*>(0xFFFFFF);
  int somenum = 12;
  r = &somenum;
  void* q = r;
  cout << (q == r ? "True" : "False") << endl;

  cout << *reinterpret_cast<int*>(r) << endl;
  return 0;
}

As evident form the above example we can easily argue that a void* pointer doesn't know about the type of object being pointed to, consequently, it can only be operated upon in a way that woud not require us to know the size of object being pointed to by such pointer or to put it simply a void* pointer can't be used to dereferce the object being pointer to by such pointer unless it has been explicitly converted to the pointer to the inteded type. However, the explicit conversion of such void* pointer to the intended type require some housekeeping while ensuring that the type of the pointer being casted is the same as the pointer to the original object being pointed to by such pointer. Converting a void* pointer to a type differing from the pointer to the original object could on the same hand results in writing off an object at a different word bit pattern that wasn't meant for the type of the object being referenced by such memory location, therefore, it can usually result in hardware error, exception or at worst memory fragmentations, etc.

Though quite strigent in derefercing operation, the operations on void* pointer that otherwise, don't require us to know the corresponding type of the object being pointed to such checking for the equivalence of two void* pointer, assignemnt to other void* pointer, etc., can be done equally as well as in the case of the other pointers to the given type.

sizes

As discussed earlier one should try to limit the implementation-defined behavior in the scope or the translation unit of a program to as greate of an extent as possible. In fact, most of the these implementation-specific properties are rather direct but other can be non-obvious, for intance the size required to represent object of any given type and hence the range of compuation for such object is also implementation-defined. C++ however, provides us with a way to check for the sizeof of any given object of the given type or in particular for a type by using the sizeof operator, which return an unsinged integer representing the sizeof of the given object. As such, though it is advisible not to make any assumption on an implementation-defined property but we can still asume that C++ char type is the smallest independtly addressable unit in a C++ program and hence can be represented in atleast a byte of data, an integer could be repsent with atleast a word or 4 bytes, a short can be represented in 2 bytes, etc. In particular one can usually assumes the following to true about the size of the given type of an object.


#include<iostream>
using namespace std;
template<class P, class S, class U>
void sizes(){
  cout << (sizeof(char) <= sizeof(bool) && sizeof(bool) <= sizeof(short int) && sizeof(short int) <= sizeof(int) && sizeof(int) <= sizeof(long) && sizeof(long) <= sizeof(long long)) << endl;
  cout << (sizeof(int) <= sizeof(float) && sizeof(float) <= sizeof(double) && sizeof(double) <= sizeof(long) && sizeof(long) <= sizeof(long long) <= sizeof(char[12])) << endl;
  cout << (sizeof(int) <= sizeof(P*)) << endl;
  cout << (sizeof(P) == sizeof(S) && sizeof(S) == sizeof(U)) << endl;
}
int main(){
  sizes<int, signed int, unsigned int>();
  return 0;
}

Note, in the last declration as mentioned above, we can deduce the sizeof any given type with its equivalent extended types(i.e. signed or unsigned) type is usually the same, but they differ in their internal representation or the range of compuation available to them. Moreover, it's worth considering that it's not always garunteed that on any given implementation the sizeof(long) is equivalent to the sizeof(long long) or sizeof an integral type is equivalent to the sizeof of the pointer to the given type, in fact, there may no pointer pointing to an odd word bit boundary, etc.

Alignment

An object just not only need enough storage to hold onto its representation, in addition on some machine architecture the bytes used to hold on to the representation of such object must be properly aligned with the corresponding word-bit boundary so as to facilitate efficient address space search for memory allocation, accessing, etc, for instance, an integer may be aligned with a 4bit word boundary, a double with 8 bits word boundary, etc. This can however, because of some inefficiency in terms of the memory expanded to represent an object of the given type, in particular the user defined type may contains holes for improvement. For example:

#include<iostream>
using namespace std;
struct Things{
    int a; //4 bytes
    char b; //4 + 1 + 2(structural padding to enforce alignment)
    float c; //8 + 4 = 12;
    char d; //12 + 1 + 3(structural padding to enforce alignment with int).
}
struct Things1{
  char b; //1
  char d;//1 + 1 = 2 + 2(structural padding)
  int a; //4 + 4 = 8
  float c; //8 + 4 = 12;
};
int main(){
  cout << (sizeof(Things) >= sizeof(Things1)) << endl;
  return 0;
}

Though for the most of the program the layout for the user-defined type are usually not that explicit to the programmer in the sense that we don't need to really fret about it too much however, where it become a little infefficiennt includes the places such as an defining an sequence of several objects of such type thereby making the memory wastage much more prelavant in such instances. For example:

#include<iostream>
using namespace std;
struct Things{
    int a; //4 bytes
    char b; //4 + 1 + 2(structural padding to enforce alignment)
    float c; //8 + 4 = 12;
    char d; //12 + 1 + 3(structural padding to enforce alignment with int).
};
struct Things1{
  char b; //1
  char d;//1 + 1 = 2 + 2(structural padding)
  int a; //4 + 4 = 8
  float c; //8 + 4 = 12;
};
float checMemWastage(int n){
  float original_size = n * sizeof(Things);
  float reduced_size = n * sizeof(Things1);
  return ((original_size - reduced_size));
}
int main(){
  cout << "Mem Wastage saved by:: " << checMemWastage(1024) << " kb" << endl;
  return 0;
}

These sort of inefficiency can be easily triffled with while just rearraging each of the members of such user-defined type in the order of theor increasing size, consequently, while doing so one can ensure that the structural padding is limited to be as wastefull as before only for the last member as opposed to each of the members of the given user-defined type.

In accordance to each of the drawback while storing the sequence of object of arbitary types, C++ also allows us to explicitly inforce the alignment for the given user defined type for which case C++ make use of the alignof and alignas operators, where alignof returns the alignment of the deduced type for the given expression. Sometime however, we have use alignment in declrations where expressions like alignof(x + y) is not allowed for instnace aligning with the size grater than sizeof any defined fudamental or built-in types. Instead we can use alignas: where alignas(T) means align just like "T".

#include<iostream>
#include<vector>
using namespace std;
struct alignas(32) Random {
  int a; //4
  char b; //8
  int c;//12
  friend ostream& operator <<(ostream& os, Random r1){
    os << r1.a << " " << r1.b << " " << r1.c << endl;
    return os;
  }
  Random(int a1 = {}, char b1 = {}, int c1 = {}): a{a1}, b{b1}, c{c1}{};
  bool operator!(){
    return !a && !b && !c ? true: false;
  }
};
template<class T, class F>
void unintialized_copy1(T begining, T ending, F* result, const F& val = F{}){
  T p;
  try{
    for(p = begining; p != ending; p++){
      ::new(result++)F{val};
    }
  }catch(...){
    for(T starting = begining; starting != p; starting++){
      starting.~T();
    }
  }
}
void use_alignments(){
  auto ac = alignof('a');
  auto ap = alignof(char[12]);
  auto ar = alignof(int*);
  int arr[2]{1, 2};
  auto arp = alignof(arr);
  cout << ac << " " << ap << " " << ar << " " << arp << endl;
  cout << sizeof(Random) << endl;
}
void use_alignments1(const int n = 1024){
  Random buffer[n];
  alignas(Random) vector<Random>user(12);
  const int copied_size = std::min(n, (int)(n / user.size()));
  cout << copied_size << endl;
  unintialized_copy1<std::vector<Random>::iterator, Random>(user.begin(), user.begin() + copied_size, buffer, Random{12, 'a', 13});
  for(size_t i = 0; i < 1024; i++){cout << buffer[i] << " ";}
}
int main(){
  vector<int>v1(12);
  use_alignments();
  use_alignments1();
  return 0;
}

Declrations

Before an object can be used in C++, it must be suitbly declared, i.e. it must be associated with a particular type for which the corresponding operation performed on such object makes sense. For each of the example mentioned below corresponds to a C++ declratioons.

int globa = 12;
void local_function(){
  int local;
  int* randomval = &local;
  delete randomval;
}
string s;
struct Date;
struct Date{
  int day: 5,
  int month: 5,
  int year;
};
const char* somename = "somename";
const double pi{3.14};
const char* seasons[]{"random", "seasons"};
template<class T> void abs(T a){return a > ? a : -1 * a;}
vector<string> somenames = {"some", "names"};
void randomfunction(int a);

As evident form the above example, a declration does more than just associate a type with a particular identifier. In particular, a declration for the most of the part is also a definition. A definition provides all that is need for an entity to be used in a program. In particular it takes memory to represent something, that memory is set aside by defintion as opposed to declrations. Thus, most of the definition are accompinied by specifying a well defined value to whatever is being declared. In that sense a declration can be considered as a part of an interface, while definition can be considered as a part of implementation. While taking that view into the consideration we try to compose interfaces out of the declrations and replicate them in separate files; defintions that sets aside memory doesn't belongs in an interface.

Of the above memntioned examples, one one i.e. void randomfunction(int a), could be considered as a declration, i.e. if used the entity may be declared elsewhere. For the effect of the rule preciding over the use of #include, any entity can have a single defintion in a given translation unit, even though they can have multiple declrations. Moreover, all of declration of an entity must agree upon its type consequently the following piece of code has two erros:

int count;
int count; //error redeclration in the same scope.

extern int no;
extern short no; //error type mismatch of external defintion.

some defintion, might explicitly speicifies a value, for instnacae:

#include<iostream>
using namespace std;
const int randomval = 12;
const double pi{3.14};
struct Date{int day: 5; int month: 5; int year;};
const char* names[]{"random name"};
int day(Date* p){return p->day;}
template<class T, class V>
V narrow_conversion(T source){
  V res = source;
  if(res == source){
    return res;
  }else{
      throw std::runtime_error{"narrowing conversion"};
    }
}
void use_explicitt_declrations(){
  cout << randomval << " " << pi << " " << endl;
  try{
    int res = narrow_conversion<float, int>(12.3f);
    cout << res << endl;
  }catch(std::runtime_error& r1){cerr << r1.what() << endl;}
}
int main(){
  use_explicitt_declrations();
  return 0;
}

For type aliases, function, template literals and const the value is constant and can't be changed in the given scope, while for non-const the value may change later on.

The structure of Declration

The structure of declration as defined by the C++ grammar has evolve over the period of time, but without making too much of radical simplification, a declration in C++ can be thought to be composed of the following five parts:

  • A base type for the object to indicate the amount of storage and the operations that can be performed on such entity.
  • An optional prefix specifier such as virtual, static, etc.
  • A declrator optionally including name such as p[7], (*), const*, etc.
  • An option suffix specifier such as const, noexcept etc, indicating an additional information about whatever is being declared.
  • An optional function body or intializer such as return{0}, ={12.3f}, etc.

Except for a function, namespace, etc., each of the declrations are generally terminated with a semi-colan.

consider the following example:

const char* teas[2]{"red", "black"};

Here, the base type is of couse const char, the declrator *teas[2], and the initializer black as specified in the intializer-list.

Usually a declrator consist of a name and optionally some declrator opertors. The most common declrator operators are as follows:

TypeDeclrator(Name)
prefix*(dereferencing operator)
prefix*const(const pointer)
prefix*volatile(volatile type specifier)
prefix&(lval ref)
prefix&&(rval ref)
prefixauto(auto type specifier)
postfix[](subscripting operator)
postfix->(indirection operator)
postfix()(function call operator)

The postfix operator binds tighter than the prefix operator, Consequently, the given notion can be exploited to facilitate the ideas of pointer to an array or pointer to a function for instnace

void somefunction(int a, int b){
  cout << (a > b ? "True" : "False") << endl;
}
void pointer_to_Arrays_and_Functions(){
  const char* persons[2]{"random", "person"}; // array of pointer to const char i.e. string literals
  const char* (*names)[2] = &persons; //pointer to array of const char
  cout << *names << endl; //implicity converts to the pointer to the first element consequently the name reffers to the pointer to the person[0] i.e. pointer to a const char*
  using PTOF = void(*)(int); //read from right to left i.e. a pointer to a function that accept and int argument and have a return type of void.
  PTOF p1 = &somefunction; //A pointer to a function is limited in the sense that it can only be used as an alternative to the call stack location of the function call, and hence can't be used to modify the code local to the function by dereferencing.
  p1(12, 13);//same as *p1(12, 13);
}

Declrating Multiple names

One can also declare multiple name within a single statement, however, it should be often be taken into the consideration that the consequent declrator operator applies to the individual names only while declaring multiple names into the single.

void declaring_multiple_names(){
  int a = 12, *ptr = &a;
  int* ptr1 = &a, ptr2 = a;
  //ptr = ptr; //error cant convert int to int*
  cout << a << " " << (ptr == ptr1 ? "True" : "False") << " " << (a == ptr2 ? "True" : "False") << endl;
}

As evident such sort declration are often dubious and potential source of errors and confusions. Consequently, one should avoid making such declration to the greatest extent possible.

Scope

Defining an entity in C++ introduce it in a particular scope, meaning it can only be used within that scope of the translation unit or a program. For instnace when considering C++ we can usually think about the following scopes:

  • Local Scope : An name declared inside of a function, or lambda expression and outside of any class, namespace, enum classes, etc., is said to have a local scope. It scope extends from the point of the declration to the end of the function or the lambda expression of its part. As such a function arguments are generally tied to the scope of the function itself, meaning they are acessible with the scope of its function as well and declared at the outermost block of the function declration.
  • Class scope: Any member, function static or name introduced within a class and outside of any other namespace, function, lambda expression, etc., is said to have a class scope. It scope extends from the point of the declration to the end of the class of which its a part.
  • Namespace Scope: A name declared within a namespace and outside of any other class, enum class, function, lambda expression, etc., is said to have a namespace scope. It scope extends from the point of declration to the end of the namespace of which its a part. Technically speaking a namespace member may also be acessible from other translation unit.
  • Global Scope: A name declared outside of any class, enum class, lambda, function, local statements, etc., is said to have a global scope. It scope extends from the point of the declration to the end of the translation or the program file of which its a part. A global name is technically still a namespace, hence it can also be accessible from other translation unints.
  • Statement Scope: A name local to the statement that can pertain to any kind of the statement as supported by C++, for instnace the iterative, conditional, switch statements, etc., is said to have a statement scope. It scope extends from the point of the declration to the end of the statement of which its a part. Moreover, since the scope an name introduced within a statement is local to the stament itself, we can multiple same identifier used within the statements that are declared within the same scope itself. This in turn allows us to conviniently use the same identifier within the different statements as opposed to fiddling with different ones.

The declration of name within a block can be hidden by another declration in the enclosing block. This shrewed yet often frowned upon way of hiding local variable is often known as the shadowing. However, it might so, that we can come across the declration with the same name as in another declration within the global scope, for which case we can explicitly specify to access the global scope's declration form within the local scope with the resoluation operator(::). However, there is no way that you can access a hidden local variable in that rough manner.


#include<iostream>
#include<vector>
using namespace std;

int global_scope;
void local_Scope(int a){
  int randomval = 12;
  cout << randomval << endl; //OK accessible within the local scope of the function.
}
class Class_Scope{
    int x, y;
    public:
      Class_Scope(int x1 = {}, int y1 = {}): x{x1}, y{y1}{
        if(x < y){throw std::out_of_range{"x > y"};}
      };
      void display(){cout << x << " " << y << endl;}
};
namespace Namespce_Scope{
  int a = 12;
}
void statement_scope(){
  vector<int>somenums(12, 2);
  for(size_t i = 0; i < somenums.size(); i++){cout << somenums[i] << " ";}
  for(size_t i = 0; i < 12; i++){if(i%2 == 0){cout << i << " ";}}//OK name with statement scopes are local to the statement itself.
}
void shadowing(){
  int global_scope = 12;
  cout << global_scope  << endl; //12
  {
    int global_scope = 13;
    cout << global_scope << endl; //13
  }
  cout << ::global_scope << endl; //0
}
int main(){
  local_Scope(13);
  //cout << randomval << endl; //error no randomval not acessible outside the local scope of the function
  Class_Scope c1{13, 12};
  c1.display(); //OK acessible within the scope of the instnace of type Class_Scope.
  //cout << x << " " << y << endl; //Error x, y are acessible only in the class scope.
  cout << Namespce_Scope::a << endl; //OK acessible within the namespace scope.
  //cout << a << endl; //Error no a declared in the given scope.
  cout << global_scope << endl;
  statement_scope();
  shadowing();
  return 0;
}

Initiailization

If an intializer is specified for an object, that intializer determines the intial values for that object. C++ offers 4 different syntaical styles of intialzation for the object of given types.

  • Direct Initialization: Mostly pertaining to the traditional C-style intialization, and allows implicit type conversion with narrowing. Syntatically speaking the direct intialization for the object of type T and intended value V can be represent as T objname = <value>
  • Constructor Based Intialization: Usually used to intialize the object of user defined type, but can also be used on fundamental and built-in types. Usually, a constructor based initialization for object of type T and intended value V can be mentioned along side: T objname(Value);
  • List-intilization: List intilization can be used explicitly to intialize a sequence of object, or for any other intilization purposes while also taking into the consideration that it doesn't allow narrowing conversion(conversions lossing infomration). In that sense a list initilization only allow initilization between related type given they they don't involves conversion from a larger type(capable of holding more data) to a smaller type. Thus, a character can be converted into an integer but not the other way around. Syntatically speaking, for an object of type T and intended value V a list based initialization can be represted as T objname{V};
  • Direct initlization for sequence of values of type T, it often allows narrowing. Each of the following way of intilization as mentioned above, except for the list-based intilization are generally susecptable to narrowing. Consequently, I often recommend using List-based intilization unless there is a good reason not to. Moreover, the constructor based intilization may also be used to provide different meaning based on the intilizer given in corresponding to the type's constructor intended for such intilization. For instance:
  • We can also often used {} to indicate the intent for default initilization.
#include<vector>
#include<iostream>
using namespace std;
void initializations(){
  int direct_initilization = 12;
  int list_initilization{12};
  //int list_initilization{12.3f}; //error doesn't allow narrowing.
  //int arr[2] = {1.2f, 2}; //Even direct list based initializations for sequences of values doesn't allow narrowing.
  //int arr1[2]{1.2f, 2};
  int f(12); //constructor based intilization can often be cofused with a function called with same set of arguments and return type.
  vector<string> alternative_constructor_base_intilization(12, "Hello");
  //vector<string> confusing_constructor_based_initilizations("Hello"); //error no vector constructor takes string arguments.
  int default_initilzation{};
}

int main(){
  initializations();
  return 0;
}

Defaut Initialization(Missing Initializer)

It is usually adviseable to be consistent about the intilization of every entity defined in the given scope. However, in the places where the user leaves out the intitializer for an explicit intent of default intilization the situation is a bit tricky. Usually the object declared in global scope, static, or a user defined type with a default constructor are intiliazed with a default values corresponding to the particular type. Even most of the fundamental type corresponds to some deafult value if no value is otherwise specified. For instance the default value for int would the most suitable representation of zero, etc. Local variable and the object created on the free store however, are not default initialized unless explicitly specified. Moreover, uninitialized variable can lead to undefined behavior and thus, should be avoided as much as plausible, as a matter fact, the only good place for an unintialized object would probally involves a large input buffer where from measurement for instance, you known that randomly intializing would've been far more unoptimal at least in terms of performace hit.

#include<iostream>
using namespace std;
class Default_constructor_class{
    int x, y;
    public:
      Default_constructor_class(int x1, int y1): x{x1}, y{y1}{};
      Default_constructor_class(): x{}, y{}{};
      friend ostream& operator <<(ostream& os, Default_constructor_class& c1){
        os << c1.x << " " << c1.y << endl;
        return os;
      }
};
void default_intializations(){
  int a1;
  int* ptr1 = new int;
  int a{};
  int* ptr = new int{};
  cout << a << " " << *ptr << endl;
  cout << a1 << " " << *ptr1 << endl;
  delete ptr; delete ptr1;
  Default_constructor_class c1;
  cout << c1;
}
int main(){
  default_intializations();
  return 0;
}

Initialzer List

For the most of the case thus far, we have intialized an object with a single initializer or no intializer. However, an equally common case can involves intializing an object with a set value value such as while declaring a sequence of object such as a vector or array. For such case C++ makes use of intializer list. For the type T and intializer list is generally used to intialize a homogenous sequence of elements of type T or it can also be used as an intializer for a user defined type in accordance to the defined constructor on such type. For Example let's consider the code fragment given below:

#include<iostream>
using namespace std;
struct Person{int a; float b;};
int main(){
  int arr[] = {1, 2};
  struct s = Person{12, 13};
  int randomval = {12};
  vector<string>names = {"random", "person"}
  vector<std::pair<int, int>>cods = {{1, 1}, {-1, 1}, {-1, -1}, {1, -1}};
  return 0;
}

In the above segment the use of = is redudant though, some tend to use it signify the a set of value has been used to intialize a set of member variables. In some case function styled intialization can also be used especially when intializing a user defined type. However, such intialization could be potentially confusion if one tend to defined a function with same signature as the given defination for instance:

complex<float> cods(1.2, 2); //could be pontentially confusion with function call complex<double> cods(int, int);

Cosnequently, one should use {}-based intialization whenever plausible, the one obvious case to beware of such intialization would include an intialization with auto where the type might not be deduced to be what the user except cause while using {}-intialization with auto, the deduced type usually turns out to be std::intializer_list<T>

Deducing a type dynamically at runtime (auto type specifier)

Sometime a programmer might not know the type of the identifier beforehand to specify it as in the case of most of the statically type language such as C++ where the type of an entity is specified beforehand at the time of its declration. For such case one would rather want the type to be deduced from the initializer itself, as the case with most of the dynamically typed language. This sort of behavior is a double edged sword because this makes our program much more lenient to the value an identifier can assume and hence lesser housekeeping operation are needed to taken care of while doing such initialization. On the other hand, it leaves the behavior of the intialized value explicitly in the hands of the programmer and hence is less stringent in the amount of the operations, and the values that can assumed by object in such dynamically typed language, therefore making it less type-safe, and harder to reason about at glance.

C++ allows us to enjoy this behavior of dynamically typed language by providing the C++ programmer with auto type specifier, that can esentially deduce the type of an identifier, const, constexpr, etc from the intializer itself. The use of such type specifier however, become much more prevalant when the deduced type is often harder for the programmer to explicitly specify or even determine. Moreover, one should avoid using auto in larger scope function as explicitly mentioning the types within the larger scope would not only help localizing debugging, but could also avoid type related error. Additionally, it is usually a good idea to keep the scope of the function as minimal as plausible either way. One can also use auto type specifier explicitly within the range for loop to deduce the type of either of the identifer being iterated upon. However, since the deduce type from a type specifier can never be reference cause reference can never be implicitly dereference in an expression.


#include<iostream>
#include<vector>
using namespace std;
void use_autos(){
  auto a = 12; //deduce to be int.
  cout << typeid(a).name() << endl;
  float f{12.3f};
  cout << typeid(f).name() << endl;
  vector<vector<int>>res{{1, 2, 3}, {2, 2, 3, 4}, {-3, 3, -4}};
  //use auto when the type is hard to deduce.
  for(vector<vector<int>>::iterator it = res.begin(); it != res.end(); it++){
    for(vector<int>::iterator it1 = it->begin(); it1 != it->end(); it1++){
      cout << *it1 << " ";
    }
    cout << endl;
  }
  //use auto in small scope to localize errors!
  for(auto& i: res){
    for(auto& j: i){cout << j << " ";}
    cout << endl;
  }
}
int main(){
  use_autos();
  return 0;
}

Auto and {}-intializations

One of the trsipy things that one might encounter while using auto type specifier generally revolves around using it with the intializer list in the sense that in such case, you might not want the type to be deduced, cause in such case an auto type specifier generally ends up with deduced type of intalizer_list<T>, this could particular missleading with just a single intializer, however, dismissing the mentioned rule for the single intializer, would essentiially render it to be less generic and equally redudant for the sequence of value. Moreover, since the auto type specifier use the type of the element of such sequence to deduce the resulting type, we can use auto type specifier with a hetrogenous list. Consequently it is often advisable to use direct intialization when working with autos, whenever one don't explicitly need the deduced type to be initializer_list itself.

#include<iostream>
using namespace std;
void auto_and_intializer_list(){
  auto ac = {1, 2, 3, 4}; //intailizer_list<int>
  auto ap = {9.2}; //intializer_list<float>
  cout << typeid(ac).name() << endl;
  cout << typeid(ap).name() << endl;
}
int main(){
  auto_and_intializer_list();
  return 0;
}

Decltype Type Specifier

In some case, we don't want the deduced type to be just the resulting type of the intializer, as opposed to that one might want the deduced type to be a part of more complicated type such as the return value from a function or an expression, for such case C++ provides the notion of declytype(expression), which help us to deduced the type to be equivalent to the resulting type of the expression specified. This is particularly usefull in generic programming where the resulting type is result of some expression and is not known before hand such as matrix multiplication for which the resulting type might be result of the type deduced from multiplication of two different elements within the matrix.


#include<vector>
#include<iostream>
#include<cassert>
using namespace std;
template<class T, class U>
auto multiply(vector<vector<T>>& v1, vector<vector<U>>& v2) -> vector<vector<decltype(T{} * U{})>>{
  vector<vector<decltype(T{} * U{})>>res(v1.size());
  assert(v1.size() == v2.size() && v1[0].size() == v2[0].size());
  for(size_t i = 0; i < v1.size(); i++){
    for(size_t j = 0; j < v1[0].size(); j++){
      res[i].push_back(v1[i][j] * v2[i][j]);
      cout << typeid(res[i][j]).name() << " ";
    }
    cout << endl;
  }
  return res;
}
int main(){
  vector<vector<int>>v1{{1, 2, 3}, {4, 3, -2}, {-1, 2, 2}};
  vector<vector<float>>v2{{-2, 2, -2}, {2, 3, 4}, {12, 33, 200}};
  multiply(v1, v2);
  return 0;
}

Lvalue and Rvalue

While talking about the object, we are generally concerned with two things, i.e. wheter the object has an identity, meaning it resides in the memory and hence can be pointed to, accessed, etc., and if its moveable, i.e. wheter it can be moved from one memory location to other therefore leaving its memory location in a valid but unspecified state. Depending on the following two construct we could generalize the objects as following:

  • An object with only identity but not moveable, often know as the lval object.
  • An object that is just moveable, often known as the pure rval object.
  • An object that is moveable but don't have any identity, often known as rval object.
  • An object that only has identity, often known as generalized lval object.
  • An object that has both identity and is moveable, often known as the exclusive rval object.

The exact meaning of exclusive rval object is rather obscure but C++ standard operation move gives us the facility to treat any lval object as exclusive rval object, while converting them to rvalue and engaging in the move operation.

The concept of references is generally discussed in regards to the fact that C++ allows to operate upon with the object either by just having to work with the copy of the object or in particular with indirection or references or address for the object of given type. Consequently, reference allows us to reffer the larger representation of the object such as vector of string, etc., at a relatively cheaper cost as opposed to copying such as object, such as while passing the copy of such object as a function arguments, etc. Moreover, if one tend to know wheter the object value need to preserved after the consequent operation i.e. if it resides in a memory location one can sunsequently treat it as lval or if its just a temporory result of compuation such as the return value from a function or a temporory const whose value we might need to preserve we can treat it as rval reference, for which case we use relatively inexpensive move operations as opposed to copy operations. Thus, generally speaking a reference can be one of the following kind:

  • Lval Refernce: Whose value we need to preserve such as an object storage in a contiguous block of memory, etc.
  • Rval Reference: Whose value we might not need to preserve and hence can employ significant optmizations in the way it's being accessed or used.
  • Const Lval References: A constant lval reference whose value can't be change in the given scope.

Moreover, since lval, rval and const lval are fundamentally different construct, we can't really use an lval ref as an rval ref and vice-versa with any explicit conversions. However, since a const lval reference can't be modified either way, we can actually use it as both lval ref and rval reference.

A Reference should not be regarded as an object itself, if anything else, it is just a linkage to an object, thus any operations performed on the references are generally reflected on the object for which its a reference. While taking that view into consideration a reference differs from a pointer in the sense that a pointer generally occupies some space in the memory and even though it is used for indirection to memory addresses, unlike refernce it itself can be considered as allocated block of storage. However, reference in that regards can't be used themselves as object even though they are linkage to an object. Thus, we can't use references for anything that would otherwise, require a real entity/object for instance using it as the element for any sequence type such as a vector, array, etc. Moreover, unlike pointer a reference is generally linked to the same object throughout its lifetime.


#include<iostream>
using namespace std;

string print_lval_names(string& fname, string& lname){
  string fullname = fname + " " + lname;
  return fullname;
}
string print_rval_name(string&& fname, string&& lname){
  return fname + " " + lname;
}
string print_const_lval_names(const string& fname, const string& lname){
  return fname + " " + lname;
}
void use_print_names(){
  string first_name = "random";
  string last_name = "person";
  cout << print_lval_names(first_name, last_name) << endl; //OK
  //cout << print_lval_names("random", "person"); //Error an lval references can't bind an rval.
  cout << print_rval_name("random", "person") << endl; //OK
  //cout << print_rval_name(first_name, last_name) << endl; //Error an rval references cant be binded to lval.
  cout << print_const_lval_names("random", last_name); //OK const lval ref can bind both lval and rval references.
}
void ref_binds_to_single_object(){
  char somename[] = "somename";
  char& randomnames = *somename;
  while(randomnames++){
    cout << randomnames << endl;
    if(getchar()){return; }
  }
}
void cant_use_ref_as_real_object(){
  int a = 12;
  int b = 13;
  int& firstelem = a;
  int& secondelem = b;
  firstelem += 1;
  cout << (a == firstelem ? "True" : "False") << endl; //whetever ops are performed on ref are reflected on object for which its a ref.
  //int& arr[2]{firstelem, secondelem}; //Error cant have sequences of references.
}
int main(){
  use_print_names();
  ref_binds_to_single_object();
  cant_use_ref_as_real_object();
  return 0;
}

Lifetime of an object

An object liftetime within a given translation unit starts when its constructor start executing and ends when its desctructor starts executing. Object of a type with a declared constructor, such as int can be considered to have a default constructor and desctructor that doesn nothing. We can classify objects on the basis of their lifetime within a program or translation unit as follows:

  • Automatic: Unless programmer specified otherwise, object declared within a function, lambda, etc., are created when their definition is encountered and destroyed when their name goes out of the scope. Such type of objects are often known automatic objects and in a typical implementation they are generally allocated onto a stack, where each of the local object typically gets it own stack frame while being pushed during the function call mechanisms and consequently realsing the stack frame memory during the pop operation as their name goes out of the scope, which is often called as returning from a function.
  • Static: Object declared in global and namespace scope and static declared in class or function are generally intialized only once, and consequently, have same memory address throughout their lifetime. These objects are often known as static objects, however, since static object can preserve their value between the function call, etc., it can cause problem in a multi-threaded enviorment where for instance a static object might be shared amongst the different thread thereby leading to data races, etc.
  • Free Store: Using the new and delete operator objects can be allocate or deallocated on the free store or heap such that their lifetime is directly controlled by the user in the sense that the user is responsible for realsing any resources required by such object before their names goes out of scope, or any other abrupt exit of the program to prevent unecessary memory leaks.
  • Temporory: These object generally include intermediate result of compuation or temporory value, etc., and generally have their lifetime that of the full expression of which their are the part or the reference to which they are bound. A full expression is an expression that is not a part of another expression.
  • Thread Local Object: These often includes the object that are declared as thread local. Such objects are created when their thread is and destroyed when their thread is.

Static and Automatic object are tradtionally reffered to as Storage classes.

Type Aliases

Type aliases as their name suggest are generally used to introduce aliases for a type in a place where:

  • The original type is too hard to type or understand in the eye of the programmer.
  • A certain implementation technique require same different to have different name in a context.
  • A Specific type may be mentioned in one place to facilitate mantainbility in program, for instnace by replacing the type definition of int to reffer to int32_t instead of plain int we can assure that every such given use of int will corresponds to int32_t instead of plain int by introducing a type alias in a single place as opposed to using replacing int with int32_t in different places in the given translation unit.
  • To hide away the implementation specific details, in the sense we can explicitly specify some type alises to prevent implementation specific behavior with some types. For instance using int32_t type alias insted of int explicitly specify the intent of int to occupy 32 bits as opposed to relying on the implementation to decide on the range available for the given integral type. For example:

#include<iostream>
#include<cstring>
using namespace std;
template<class T>
class Points{
  private:
    T x, y;
    using Value_type = T;
  public:
    Points(T x1 = {}, T y1 = {}): x{x1}, y{y1}{
      if(typeid(Value_type).name() == typeid(int{}).name() || typeid(Value_type).name() == typeid(float{}).name()){}
      else{
        throw std::runtime_error{string{"incomptaible type, expect Arthematic type got "} + typeid(Value_type).name()};
      }
    }
};
int main(){
  //Points<const char*> p1{"12", "12"};
  Points<int> p2{12, 12};
  Points<float> p3{12.3f, 2.2f};
  return 0;
}

For good or bad type aliases are generally used as aliases for the distinct type rather than different type, meaing they reffer to the type for which they are the aliases for instance:

using Pchar = const char*;
pcahr p1 = nullptr; //const char* p1  = nullptr;
using PTOF = int(*)(int, int);
PTOF p1 = &somefunction; //OK

An older sytax of using type aliases would involve using typedef and placing the alias name being used where it would be used in the declarion of a variable. For instance:


typedef long long int32_T; //equivalent to uisng int32_T = long long;
typedef void(*PTOF)(int); //equivalent to using PTOF = void(*)int;
typedef const char* pchar; //equivalent to pchar = const char*
#include<iostream>
using namespace std;
void somefunction(int a){cout << a << endl; }
int main(){
  int32_T l1 = 123ll;
  PTOF p1 = &somefunction;
  p1(13);
  pchar c1 = nullptr;
  return 0;
}

As Evident from the forementioned example type aliases can also be used to create template aliases. However, we cant apply type specifier to a type alias for instance the following fragment of code would result in a sytax error.

using Char  = char;
usinged Char = 'a'; //error cant use type specifier with type aliases.

This Blog was created with sole intent of teaching and learning programming as such, this is a sole ditch effort, so please forgive any mistakes senpais. For further suggestion please contact via Email in the footer! 😁