- Published on
Structure, Enumerations, and Unions
Introduction
The Effective way of using CPP in general and one of the key ascpect that seprate CPP from C in a crude sense is it ability to define user-defined types to hold onto the representation of the data, and hence introducing the ability to have a level of abstraction while dealing while dealing with most of the language construct and therefore, making it much more easier to use by providing just the interface to the operation supported on the data that can be representaion of the given user defined type as opposed to boggin the user with the implementation details of such operations. As such we'll deal with three most primitive variants of declarling user-defined in CPP in the following discussion. Though each of the variant have their equivalent counterpats in C language itself, but they could be considered as backbone upon which the current concepts of Object Oriented Programming with Classe are formulated in CPP.
- Structure: Structure are one of the most primitive way of defining user define type and can be thought of as way to hold onto the sequence of elements of arbitary type corresponding the data structure required by the user.
- Union: A union can be thought of a struct where each of the member are allocated at the same memory address and hence we can only access a member of union at a time. Union for the most of the case are often used as an optimization aid rather than anything else, and doesn't really find much of use in a higher level of programs. More so, an excessive use of union can make the program relatively harder to use and comprehend as opposed to just sticking to using the classes or struct for defining user-defined types in CPP.
- Enumerations: An enumeration can be thought of a set of named integral constant values called enumerators that are usually used as a flgged values to represent constant integral values used in the program as opposed to introducing magic number throughout the program, thereby making it much more harder to read or wrap your head around.
- Enum class: An enum class can be thought of as a set scopped enumerators that can only be access within the enumerators and doesn't implicitly converts to integral value much like their plain enumerator counterparts.
With the rough introduction of the consequent topics to be dicussed in the preceeding sections the outline for the rest of the sections is as mentioned below:
- Introduction
- Structure
- Struct Layout
- Structure Name
- Structure and Classes
- Array of structure
- Type Equivalence
- Plain Old Data
- Unions
- Enumerators
- Plain Enums
- Enum class
Structure
- In simpliest term much like how you can think of an array as an Sequence of the object of similar type in the memory the struct could be considered as a sequence of objects of arbitary type in the memory. For example:
- The below mentioned example declares the structure holding onto the representation of Address user-defined type. Consequently, we can say that a struct is used to represent sequence of arbitary type or data that can't be represented directly by other fundamental and user-defined and is not availbale for use without the prior declarion the way fundamental type, and hence is often know as a user-defined type.
- Object of structral type can be accessed much like object of any other type, with their of course, corresponding to the structure name, and each of the individual element often known as the member of the given structural type can be accessed with the subsequent memberof(dot) operator on the object of given type.
- As such, a user-defined type generally names a type and hence object of the given type can be used much in similar way how you use an object of any other fudamental type, Consequently it can passed in as a function argument, returned as value from a function, passed in a reference as refernces(either as tempory exploiting rval reference, or lval reference), and even acess through indirection via pointer to the given type.
- When acessed through the pointer however, we genrally need to use the
(->)
indirection operator as opposed to the(.)
operator for individual access to members defined on the given user-defined type.
#include<iostream>
#include<vector>
//#define Error 12;
using namespace std;
struct Address{
int no;
const char* name;
const char state[2];
int zip[4];
};
void AccessByValue(Address a1){
cout << a1.no << " " << a1.zip[0] << a1.zip[1] << a1.zip[2] << a1.zip[3] << " " << a1.state[0] << a1.state[1] << " " << a1.name << endl;
}
void AccessByLvalReferences(Address& a1){
cout << a1.name << " " << a1.state[0] << a1.state[1] << " " << a1.no << " " << a1.zip[0] << a1.zip[1] << a1.zip[2] << a1.zip[3] << endl;
}
void AccessByRvalReference(Address&& a1){
cout << a1.name << " " << *(a1.state) << *(a1.state + 1) << " " << a1.no << endl;
}
void AccessByPointer(Address* a1){
cout << a1->name << " " << a1->state[0] << a1->state[1] << " " << a1->no << " " << a1->zip[0] << a1->zip[1] << a1->zip[2] << a1->zip[3] << endl;
cout << (*a1).name << " " << *((*a1).state) << (*a1).state[1] << " " << (*a1).no << " " << (*a1).zip[0] << (*a1).zip[1] << *((*a1).zip + 2) << (*a1).zip[3] << endl;
}
Address Change_Address(Address& current){
Address next = {12, "Random", {'N', 'Z'}, {1, 2, 3, 4}};
Address curr = next;
#ifdef Error
current = curr; //Error reintialization a non-static const member.
#endif
return next;
}
void UseAddress(){
Address a1{123, "random", {'N', 'Z'}, {1, 2, 3, 4}};
AccessByValue(a1);
AccessByPointer(&a1);
AccessByLvalReferences(a1);
AccessByRvalReference({123, "random", {'N', 'Z'}, {1, 2, 3}});
#ifdef Error
Address a2{123, "random", "NZ", {1, 2, 3, 4}}; //Error can't assing an object of type char[2] to const char*
#endif
Address random_addr = Change_Address(a1);
cout << random_addr.no << " " << *(random_addr.zip) << random_addr.name << endl;
}
int main(){
UseAddress();
return 0;
}
In the above declaration We can't pass the string literal to intialize the state member variable cause C-style string are often stored as a null terminated sequence of characters, consequently, they generally have a lenght offest by one of their original lenght, and since when explicitly specified with the bound, we can't really assign sulprus element than the bound specified we can really assign a character literal with the actual size of two to a type const char[2]. Though such use of string literals is relatively low level and can often be circumvented with the equivalent use of std::string or event c-style string literal, However I delibrately presented the use of rather low-level type for member to illustrate how it is done and what problems it presents.
Struct Layout
The object of the struct holds its member in the order in which they are declared this can cause some optimization issue in the way the corresponding struct be laid down in memory or the amount of memory allocated to the entire representation of the struct. In particular the amount of the memory allocated to the entire representation of the struct might not be what the you would have expected by adding the memory allocated to each of the members of the given type. This is generally because each of the object of the given type are allocated at certain word-bit boundary facilitate the easier serach and access for the availbale memory location for such object. For instance an integer may be allocated to a 4-bit word-boundary, a double to 8-bit word boundary, a character to 1-bit word-boundary, and there might be no implementation where pointers are allocated at odd addresses. Consequently, a struct might involves extra structral padding to align each of its member at the given word-bit boundary so as allow efficient access of such members. For example:
#include<iostream>
using namespace std;
struct ReadOut{
int a; //4bits
char b; //4 +1 + 3(structral padding to align with 4 bits word boundary for next integer).
int c; //8 + 4 = 12 bits.
char d; //12 + 1 + 3(structral padding to align with 8 bytes word boundary for next long int)
long int f; //16 + 8 = 24.
char g; //24 + 1 + 7(bits structral padding for next element if used in the sequence of the given type. )
};
struct Optimzation{
char b; //1
char d; //1 + 1 = 2
char g; //2 + 1 + 1(structral padding for next integer literal)
int a; //4 + 4
int c; //8 + 4 = 12 + 4(structral padding for next long integer literal);
long int f; //16 + 8 = 24
};
template<class T>
T abs1(T a1){
return a1 > 0 ? a1 : -1 * a1;
}
float memWasted(float r1, float o1){
float total = r1 + o1;
return (abs1(r1 - o1)/total) * 100;
}
int main(){
cout << sizeof(ReadOut) << endl;
cout << sizeof(Optimzation) << endl;
cout << "Mem Wasted:: " << memWasted(sizeof(ReadOut), sizeof(Optimzation)) << endl;
return 0;
}
Note the extra structral padding at the end of the given declaration of each of the corresponding Structral type, the padding corresponds the possibility of the next element that might be there in sequence of such type.
Structure Name
The Name of the structure become available for use immidiately after the declaration. Meaning we can use the corresponding structure name even before its complete declration as long as we dont need to the know the sizeof the correspond object of the given type. This sort of notion is often exploited to have a reference to the pointer to the object of the same type as the given structure itself for instance while defining a user-defined type for a Node of a tree having a pointer to the left and the right Node. Thus we can use the pointer to the user-defined type without complete declaration, however such object are said to be incomplete object, and hence can be accessed in whatever way that would require us to know the sizeof the object, for instance dereferencing the pointer to given type, accessing object of incomplete type, etc. For example lets consider the following example:
struct Link{
Link* head;
}; //incomplete declration of the object.
Link* useLink(Link* res){
return res;
}
void useIncompleteDeclarations(Link* head){
Link* res = head; //OK doesn't need to know the sizeof the object being pointed.
Link* res1 = useLink(res); //OK still dont need to know the sizeof of object being pointed to by link.
#ifdef errors
Link res1; //error can't create the object of incomplete type cause there is no way that the compiler can determine the sizeof such object to allocate the requisite memory.
res->data = 12; //error cant access member of incomplete type.
#endif
}
#ifdef Errors
struct LinkTree{
Link* head;
LinkNode* children; //Error LinkNode not declared in the given scope.
int data;
};
#endif
int main(){
Link* head =nullptr; //OK dont need to know the sizeof the obejct being pointed to.
useIncompleteDeclarations(head);
return 0;
}
However, despite of the fact that, we can't use incomplete declration in a way that would require us to know the sizeof the object, and can still the pass the pointer to such declration though cause the pointer are nothing but integral descriptor to the memory location where the actual object is being store, and therefore without involving in dereferencing operation for the same, one need not to know the sizeof the object being pointed, yet the declration rule still applies even to the incomplete declration meaning we can use the given pointer to the incomplete type before its inteded declration.
For the Reasons that goes into the prehistory of CPP, CPP allows declration of user-defined type and non-user defined type with the same name, with each of the them being distinguished with the prefix of the given type. However, one should not explicit in such overload of user-defined type names by making such disambiguation explicit.
Structure and Classes
A struct is simply a class where each of the member are public by default, in fact we can have any construct that we could otherwise, have with classes, and the concept of class was introduced while taking into the consideration the need to make the seperation of concern between the data representation by the user-defined type, and the operation availbale on such data explicit so that the data represented can only be access within the scoped specified by the given class and not outside of the given scope., In that sense if specified to be private for access specifier, the corresponding member of the given type might not be accessible outside of the scope of the given class, however, when specified public it may be accessible outside of the given scope through the interface provided by the given class.
As far as corelation with struct and class is concerned, there is no explicit disambiguation between the two except for the fact mentioned above, in fact, you can have struct with member function or even constructor. However, since each of the member of struct are public by default one might not need to define a constructor to explicit intialize the member from outside of the given scope of the struct decrlation as they could be accessed with the Object of the given structural type either way. Consequently I tend to use struct only in cases where I tend to define data-only type without any operation defined on the data to be hold by such type. And for the rest of the purpose one can go about using classes for notational and conventional reasons for the existence of classes in the first place in CPP.
A constructor may not only be used for intializing the data member, or member variable for the object of given type, it can for the most the case also be involved in establishing invariant to consider the instance of the object valid, reorder arguments, validate arguments, modify arguments, etc.
#include<iostream>
#include<cstring>
using namespace std;
struct Address{
const char* name;
int zip[4];
const char* state;
const char* town;
//reorder arguments, validate arguments, establish invariant modify arguments.
Address(const char* name1, const char* town1, const string& zip1, const string& state1): name{name1}, town{town1}, state{state1.c_str()}{
const int length = strlen(state);
switch(length){
case 0: case 1: {
throw std::runtime_error{"invalid length"};
} case 2: {break;}
default:
throw std::runtime_error{"cant have this length"};
}
switch(zip1.size()){
case 0: case 1: case 2:
throw std::runtime_error{"invalid length"};
case 3: {
for(int i = 0; i < 4; i++){
if(i == 0){zip[i] = 0;}
else{
zip[i] = zip1[i-1] - '0';
}
}
break;
}
case 4: {
for(int i = 0; i < 4; i++){
zip[i] = zip1[i] - '0';
}
break;
}
default:
throw std::runtime_error{"invalid length"};
}
};
friend ostream& operator<<(ostream& os, const Address& addr1){
os << addr1.town << " " << addr1.zip[0] << addr1.zip[1] << addr1.zip[2] << addr1.zip[3] << " " << addr1.name << " " << addr1.state;
return os;
}
};
int main(){
try{
Address addr1{"random", "town", "1234", "NZ"};
cout << addr1 << endl;
Address addr2{"random", "town", "123", "NZ"};
cout << addr2 << endl;
Address addr3{"random", "town", "NZ1", "123"};
}catch(std::runtime_error& r1){
cerr << r1.what() << endl;
}
return 0;
}
Array of structure
Naturally, since struct are used for nothing but to create the object of the user-defined type, we can have array of struct or struct of array. An array of struct can be create much like any explicit definition of array of given type. However, you can also conviently encapsulate the relatively lower-level array of the given type to a struct. Consequently, easing it relative use in the program. In particular such encapsulated version of array can be assigned to array of similar type, can be check for equality, and doesn't even implicit converts to the pointer to the first element. Consequently, one can also specify the sizeof the array implicitly rather than explicitly providing the same, However, since Array are considered fixed length sequence of object in memory, if one tend to used std::Array implementation to have a counterpart to an array encapsulated in a well-defined data-structure, one might still need explicitly provide the size to the array, however, using std::Array can be adventegous in the fact that it's a proper object type Consequently we can assume the following for std::Array:
- An Array name for the std::Array doesn't implicitly converts to pointer to the first element on slightest provocation, and therefore it can effective be passed by value or reference to a function argument.
- Since the std::Array corresponds to the proper object type, it generally have the copy and move constructor defined on it, and any given object of Std::Array type can be assigned to each other as opposed to the built-in array which might not allows for assignment between two arrays, even if they happens to be same type and having same no. of elements.
- However, one of the obvious disadventage of using std::Array often lies in the fact we need to explicitly pass the sizeof Array, and as such we can't even deduce the number of the elements for the array from the subsequent intializer to that array.
#include<iostream>
#include<cassert>
using namespace std;
struct Points{
int x, y;
friend ostream& operator<<(ostream& os, Points& p1){
os << '{' << p1.x << "," << p1.y << "}";
return os;
}
};
template<class T, size_t N>
struct Array{
T elem[N];
T* begin(){return &elem[0];}
T* end(){return &elem[N];}
size_t size() const{return N;}
T& operator[](const int index){
if(index < 0 || index > size()){
throw std::out_of_range{"Array index out of bounds"};
}
return elem[index];
}
Array<T, N>(T elem1[N]){
for(size_t i = 0;i < N; i++){elem[i] = elem1[i]; }
}
Array<T, N>(std::initializer_list<T> elem1){
int count{};
assert(N == elem1.size());
for(auto& i: elem1){
*(elem+count) = i;
count += 1;
}
};
void ReflectPoints(){
for(size_t i = 0; i < size(); i++){
decltype(elem[i].x) temp = elem[i].x;
elem[i].x = elem[i].y;
elem[i].y = temp;
}
}
};
int main(){
Points parray[3]{Points{1, 2}, Points{2, 3}, Points{3, 4}};
Array<Points, 3> arr{{1, 2}, {2, 3}, {3, 4}};
Array<Points, 3>arr1{parray};
arr1.ReflectPoints();
for(size_t i = 0; i < arr.size(); i++){cout << arr[i] << endl;}
for(auto& i: arr){cout << i << " "; }
for(size_t i = 0; i < arr1.size(); i++){cout << arr1[i] << endl;}
#ifdef Errors
Points parray[]{{1, 2}, {2, 3}, {3, 3}};
Array<int>arr={{1, 2}, {2, 2}, {3, 3}}; //error cant deduce no of elements from intializer. No template argument match the given constructor call.
#endif
return 0;
}
Type Equivalence
- Since structs are used to create user-defined type corresponding to the data-structure provided by the user no two structs could be equivalent of each other even if they happens to be have same member with same type.
- A struct is also different from the type specified by its members.
#include<iostream>
using namespace std;
struct s1{int x, y;};
struct s2{int x, y; };
int main(){
#ifdef errors
cout << (s1 == s2 ? "True" : "False") << endl; //Error no equivelence operator == is defined on operands of type s1 and s2.
s1 X;
int i = X; //error cant convert Object of type s1 to int.
#endif
return 0;
}
Plain Old Data
- Sometimes its help to consider data just only a contiguous chunk of storage in memory without any advance user-defined semantic defined on it. This is particularly usefully when optimizing for the move operation at a relatively lower level of the code where the real hardware resources are manipulated.
- Knowing wheter the data could be plain old data can helps in optimizing how the given data is being read or move in particular, for instance most of the plain old data can be moved from the relative memory location to another with memcpy operations which involves a single block move instruction and henec is relatively faster than let say n no. of call to the copy or the move constructor.
- Such optimizations are relatively hard to spot even in the presence of inlined optimizer, therefore looking for such optimzation could significantly helps in churning maximum bit of performance out of the system.
- But as usually these optimization are often used manipulate resources at a relatively lower level of the code and should be avoided at higher level of system.
- Usually for a data to consider Plain old data it should be trivially copyable, have a trivial default constructor, and should have a non-trivial user-defined copy semantics.
- A User defined type on the other is considered to be trivially copyable if it has:
- A trivial default constructor, meaning a constructor that does nothing, one can often use
(=default)
to define such constructor with minimal hassle. - A trivial copy and move operations.
- A trivial default constructor, meaning a constructor that does nothing, one can often use
- More so, a type is not considered to have standard layout unleses it speecify the following properties:
- Has a non static member, or baes that is not standard layout.
- Has a virtual function
- Has a reference members
- Has a virtual base.
- Has multiple acess specifier to non static data.
- To put it simply a type is considered to have standard layout, if it has obvious equivalent in C and is in the union of what common C++ Application Binary Interface can handle.
- A Type however is consider trivially copyable if it specifies the following properties:
- If it has a virtual base.
- If it has virtual function.
- If it has base or member that is non trivial.
Usually speaking, it quite a duanting task to remember all of these specified rule to come to conclusion if something can be considered Plain old data or not. However CPP implementation provides a type predicate that can use to check property on the given type here wheter the given type is a POD or not call std::is_pod<T>
. Thus, for a type T, the explicit qualification of ::value
scope resolution operator on std::is_pod<T>
would return true if the type corresponds to a plain old data, otherwise it will return false. For Example lets consider the following Example:
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
template<class T>
void check_pod(T* to, T* from, int count){
if(std::is_pod<T>::value){
cout << "Trivially copyable" << endl;
std::memcpy(to, from, count * sizeof(T));
}else{
cout << "Not a Plain odd data " << endl;
for(int i= 0; i < count; i++){
to[i] = from[i];
}
}
}
template<class T>
class Points{
T x, y;
public:
Points(int x1, int y1) : x{x1}, y{y1}{};
Points() = default;
friend ostream& operator<<(ostream& os, Points& p1){
os << "{" << p1.x << " " << p1.y << "}";
return os;
}
};
template<class T>
class Points1{
T x, y;
public:
Points1(int x1 = {}, int y1 = {}): x{x1}, y{y1}{
if(x < y){throw std::runtime_error{"cant have this"}; }
}
friend ostream& operator<<(ostream& os, Points1& p1){
os << "{" << p1.x << "," << p1.y << "}";
return os;
}
};
template<class T>
void swapping(T a, T b){
}
template<class T>
class Points3D{
int x, y, z;
int&& ref = 0; //Not a POD have a reference member.
public:
Points3D(int x1, int y1, int z1): x{x1}, y{y1}, z{z1}{};
Points3D() = default;
virtual int printSum(){
ref = x + y + z;
return ref; //not a trivial type got advances semantic with virtual function, and a reference member.
}
//copy constructor : Not trivially copyable(user-defined copy semantic for the given type)
Points3D(const Points3D& p1){
x = p1.x;
y = p1.y;
z = p1.z;
ref = p1.ref;
}
Points3D& operator=(Points3D& p1){
x = p1.x;
y = p1.y;
z = p1.z;
return *this;
}
//Not trivially moveable: user defined move semantics
Points3D(Points3D&& p1): x{std::move(p1.x)}, y{std::move(p1.y)}, z{std::move(p1.z)}, ref{std::move(p1.ref)}{};
Points3D& operator=(Points3D&& p1){
swap(*this, p1);
return *this;
}
friend ostream& operator<<(ostream& os, Points3D& p1){
os << "{" << p1.x << "," << p1.y << "," << p1.z << "}" << endl;
return os;
}
};
void usePod(){
int arr[4]{1, 2, 3, 4};
int arr1[4];
check_pod(arr1, arr, 4);
Points<int>arr2[4]{{1, 2}, {2, 2}, {2, 4}, {1, 2}};
Points<int>arr3[4];
Points1<int>arr4[4]{{12, 12}, {13, -2}, {-2, -2}, {12, 11}}; //Not a POD, has a non-trivial default constructor.
Points1<int>arr5[4];
Points3D<int>arr6[4]{{1, 1, 1}, {2, 3, 12}, {3, 3, 12}, {4, 12, 123}};
Points3D<int>arr7[4];
check_pod(arr3, arr2, 4);
check_pod(arr5, arr4, 4);
check_pod(arr7, arr6, 4);
for(size_t i = 0; i < 4; i++){
cout << " " << "arr:: " << arr[i] << " ";
cout << " " << "arr3:: " << arr3[i] << endl;
cout << " " << "arr4:: " << arr4[i] << endl;
cout << " " << "arr7:: " << arr7[i].printSum() << endl;
}
cout << endl;
}
int main(){
usePod();
return 0;
}
In the above-mentioned example, only arr1, arr2, arr3, and arr4
corresponds to the POD while other can't be considered POD.
Unions
- One can think of user-defined type similar to struct where each of the member are allocate the same memory address, meaning if take pointer to any of the member of the union each of the them possibly point to the same memory location and hence can be referenced with same pointer indirection.
- However, since each of the union members points at the same memory location, we can only access a single member of the union at a time.
- Conseqeuntly, what is being read to a union member should exactly be the same as what is being written through it, especially if each of the members have differing types, cause you each consider that since each of the member of the different types will be allocated at differnt word bit boundary, accessing a member with differing type through a memory location that wasn't aligned that way could lead to unpredicate results.
- Moreover, since each of the member of the union are allocated at the same memory address and can be accessed only one at a time, union allows for signifcant space optimization in the way its member is being stored specially in the cases, where the user construct require them to access only one of the member at the time, and therefore they can optimize away by storing each of the members at the same memory location as opposed to different one, or indirectly speaking by using unions as oppose to the other user-defined type.
- Now since each of the member for a union would be allocated the same memory location, we need to make sure that the union would be able to account for its largest member type, consequently, the sizeof the union type is the maximum of all of members.
- With that said unions are often designed for optmization aid only, and hence they don't support any advanced user-defined semantics that deviates them from being used just as a plain old data, or so to speak attest to it basic idea of allocating each of the members at a same memory location and accessing them one at a time.
- Consequently, we can't have any user-defined semantics such as copy constructor, move constructor, or any overloaded instance of constructors, virtual function, reference members, etc., for a union type by default and these operations are deleted by default for unions. Cosneqeuntly, we can't even have any other user-defined type or proper object type as defined by the standard to be used as a union member cause such operations would be deleted even for those members when used in uinons.
- With that said I consider union as somewhat of an overused features that introduce a clump of complexity in the given code without providing much of benefits in lieu of using more traditional user-defined type such a struct or class, and hence often refrain from using them. But it doesn't hurt knowing that they exist as a language construct either way. As a matter of fact that only good case that come to my understanding in times of using uinion and many others have already pointed the same involves storing color channels to represent pixel values on the screen.
- We can also define a union such that each of its member can be used just as object themselves as opposed to being referenced with the Object of the particular union type by skipping the Union Name, such sort of unions are known as
annonymous unions
and often find their use inDiscriminated union
, which we will discussed after the following Example:
#include<iostream>
using namespace std;
enum class Type{STR, INT};
struct Data{
Type t1;
string s;
int n;
void use(){
if(t1 == Type::INT){
cout << n << endl;
}else{
cout << s << endl;
}
}
Data(Type t2, int n1): t1{t2}, n{n1}{};
Data(Type t2, string s1): t1{t2}, s{s1}{};
};
struct Data1{
Type t1;
//Annonymous union where each members can be used as an object themselves.
union{
int n;
const char* s;//remember we cant have a proper object type with default constructor as in string here cause those user-defiend semantic would be deleted operations in a union
};
void use(){
if(t1 == Type::INT){
cout << n << endl;
}else{
cout << s << endl;
}
}
Data1(Type t, int n1): t1{t}, n{n1}{};
Data1(Type t, const char* s1): t1{t}, s{s1}{};
};
union channels{
char r;
char g;
char b;
};
union related_values_with_different_types{
int n; //integral type.
float f; //integral type(basically arthematic)
};
#ifdef Errors
union Deleted_Operations_and_ProbWithUnions{
int a = 12;
int b = 123; //error cant have more than 2 in class intializer.
int&& ref = 13; //cant have a reference type.
virtual void virfunction(){return ref;}
string s; //cant have a type with any advance user-defined semantics such as user-defined constructor, default construct, copy constructor, etc.
}
#endif
void use_Data(){
//As evident from the given example we can only use a one of the struct member at a time hence we can do away with singinficant space optimization by declaring them in union where each of them would be allocated at the same memory address and can be accessed only one at a time, as ooposed to having two different copy of members defined on the given type.
cout << __FUNCTION__ << " " << __LINE__ << endl;
Data d1{Type::INT, 12};
Data d2{Type::STR, "something"};
d1.use();
d2.use();
Data1 d3{Type::INT, 12};
d3.use();
Data1 d4{Type::STR, "random"};
d4.use();
//Ensure what is being written through in the union can be allocated at the same word-word bit address as what is being written to get the well-defined values without explicit conversion.
cout << d3.n << endl; //OK what is being written through is exactly what is being read through.
//cout << d3.s << endl; //undefiend values.
//cout << *reinterpret_cast<const char*>(&d3.s) << endl; //OK to do explicit type conversion to the given word-bit pattern to what is being read through with related type.
channels c1;
c1.r = 'r';
cout << c1.r << endl;
cout << c1.g << endl; //OK still a well-defined previously assigned value cause each of the union members are of same type, and hence can be aligned at the same word-bit addresses. Conseqeuently reading it off the previous address would lead to any conversion to undefined values.
related_values_with_different_types r1;
r1.n = 12;
cout << r1.n << endl; //OK what is being written through is exactly what is being written.
r1.f = 12.3f;
cout << r1.f << endl; //OK what is what read through is exactly being what is written.
cout << r1.n << endl; //Undefined reading integral value through a word-bit patter for floating point addresses.
cout << *reinterpret_cast<float*>(&r1.n) << endl; //OK explicit type conversion from integral address bit pattern to float address bit pattern i.e. value being read through.
cout << (sizeof(r1) == max(sizeof(int), sizeof(float))) << endl; //union use max of the sizeof member types so that it can represent each of its members.
}
int main(){
use_Data();
return 0;
}
- However much like for any given type in CPP, if you want to circument the default behavior for a given user-defined type such as unions specially for it to incorprate using other user-defined type with advanced smenantics such as user-defined copy, move, default and overloaded constructors, you can esnetially do that by defining what call as a discrimited union and provided those deleted operations on that type.
- A discrimited union on the other hand is nothing but an annonymous union encapsulated within a better behaved user-defined type such as a struct or a class.
- For example let's consider the following:
#include<iostream>
using namespace std;
enum class Type{INT, STR};
struct Error:std::runtime_error{
string s1;
Error(string message): s1{message}, runtime_error{message}{};
virtual void what(){cout << s1 << endl;}
};
class Discriminated_unions{
private:
Type t;
union{
int n;
string s;
};
public:
Discriminated_unions(int n1, Type t1);
Discriminated_unions(string s, Type t1);
Discriminated_unions(Discriminated_unions&& d1);
Discriminated_unions(const Discriminated_unions& d1);
Discriminated_unions& operator=(const Discriminated_unions& d1);
Discriminated_unions& operator=(Discriminated_unions&& d1);
int get_int()const;
string get_string()const;
~Discriminated_unions(){}
};
Discriminated_unions::Discriminated_unions(int n1, Type t1){
switch(t1){
case Type::INT: {
t = Type::INT;
n = n1;
break;
}
case Type::STR: {
t1 = Type::INT;
t = Type::INT;
n = n1;
break;
}
}
}
Discriminated_unions::Discriminated_unions(string s1, Type t1){
switch(t1){
case Type::INT: {
t = Type::STR;
t1 = Type::STR;
new(&s)string{s1};
break;
}
case Type::STR: {
t = Type::STR;
new(&s)string{s1};
break;
}
}
}
Discriminated_unions::Discriminated_unions(Discriminated_unions&& d1){
switch(d1.t){
case Type::INT: {
if(t == Type::INT){
n = std::move(d1.n);
break;
}
t = Type::INT;
n = std::move(d1.n);
break;
}
case Type::STR: {
if(t == Type::STR){
new(&s)string{std::move(d1.s)};
}
t = Type::STR;
new(&s)string{std::move(d1.s)};
}
}
}
Discriminated_unions::Discriminated_unions(const Discriminated_unions& d1){
switch(d1.t){
case Type::INT: {
t = Type::INT;
n = d1.n;
break;
}
case Type::STR: {
t = Type::STR;
new(&s)string{d1.s};
break;
}
}
}
Discriminated_unions& Discriminated_unions::operator=(const Discriminated_unions& d1){
switch(d1.t){
case Type::INT: {
t = Type::INT;
n = d1.n;
break;
}
case Type::STR: {
t = Type::STR;
new(&s)string{d1.s};
break;
}
}
return *this;
}
Discriminated_unions& Discriminated_unions::operator=(Discriminated_unions&& d1){
switch(d1.t){
case Type::INT: {
t = Type::INT;
n = std::move(d1.n);
break;
}
case Type::STR: {
t = Type::STR;
new(&s)string{std::move(d1.s)};
break;
}
}
return *this;
}
int Discriminated_unions::get_int()const{
if(t == Type::INT){return n;}
throw Error{"can't read and write through the different types in union"};
}
string Discriminated_unions::get_string()const{
if(t == Type::STR){
return s;
}
throw Error{"can't read and write through the different type in uinon"};
}
int main(){
Discriminated_unions d1{12, Type::INT};
Discriminated_unions d2{"random", Type::STR};
cout << d1.get_int() << endl;
cout << d2.get_string() << endl;
Discriminated_unions d3{Discriminated_unions{12, Type::INT}};
cout << d3.get_int() << endl;
#ifdef errors
cout << d3.get_string() << endl;
#endif
Discriminated_unions d4{d2};
cout << d4.get_string() << endl;
Discriminated_unions d5{d1};
cout << d5.get_int() << endl;
// cout << d5.get_string() << endl;
Discriminated_unions d6 = Discriminated_unions{12, Type::INT};
cout << d6.get_int() << endl;
Discriminated_unions d7{Discriminated_unions{"random", Type::STR}};
cout << d7.get_string() << endl;
return 0;
}
In the above-mentioned example one can easily conclude that we have used a discrimited union to override all of the problems that stems from the use of unions, in particular we have defined the deleted operations on such use of annonymus unions thereby enabling us to use the handy Std library string type without any deleted copy, move or default constructors as opposed to be fidling with the inherently low-level C-style string literals.
Enumerators
- In C++ Enumeration are considered as user-defined type cause they must be defined by the user as opposed to be being availbale for the use the way fundamental types are, however, enumeration are rather limited in their use like most the user-defined type. In fact, enumerations are often used to descibe set of integral constant that can be used as a flgged types instead of throwing magic numberic constants in the code. Each of the members of enumerations are reffered to as enumerator and their value can be of any integral type i.e. bool, char or int but not any other type.
- As such C++ implementation supports two type of enums now, i.e. plain enums and enumeration class which were introduce to address problems with plain Enums.
Plain Enums
- A plain can be used to define a set of integral constant as described above, as such if not explicit specified the type, a plain enum can assume integer value by default, meaning you can explicitly assign integr values to such enums. However, when explicitly mentioning the type, providing a value to an enumerator other than the mentioned type can lead to implict conversion and hence implemention-defined result for the consequent enumerator values.
- While no explicitly mentioning the type an enumerator value is of integer type and start from 0 to no of enumerator defined in the order of their declrations. However, when explicitly mentioning the value for such enumerator their value can range from
-2^k to 2^k
, when any of the enumerator is provided with the -ve integral value or0 to 2^k
when all of the enumerators are postive. Here, the constantk
corresponds to the least amount of bit required to represent the maximum value on either of +ve or -ve side with the specified enumerators. - Both the above mentioned points holds equally true for enum classes as well.
- As such despite their widespread use, plain enumerators suffers from two major problems for which case C++ introduces enum classes. The said problems are as mentioned below
- If the type of enumerators is explicitly specified, each of the enumerators implicitly converts to the equivalent value corresponding to its type, i.e. if explicitly specified it converts to the equivalent value in the corresponding type oherwise, the equivalent of the viable representation of 0 in the mentionded type. If however, an enumerator is not explicitly specified with any type, it can implictly converts to the integr equivalent where the corresponding integral value starts from 0 to no of enumerator or 0 to Number of enumerator while excluding any enumerator whose value has been explicitly specified.
- A plain enum is usually not scopped to the enum itself, meaning it can be accessible outside of the corresponding scope of the enum throughout the translation unit in which it has been defined, this also means that such enumerator can be used as individual integral object themselves, without any explicit reference from the object of enum type.
- As such since an enumerator declration within the enum can leak outside of the scope of enum throughout the translation unit in which it has defined it can usually lead to what we call as the
namespace pollution
, i.e. confliction declration with the same name as specified by the enumerators defined in the given enum. This was primary reason why people were kinda iffy about using enumerators in the earlier phase of their introduction in C, however, CPP provides the better alternative to enums for the exact same reason. And now, one must only consider knowing about enums as just a thing of past, as such even if they exist in the current CPP implementation for a wide variety of reasons such as maintianing compability with old C, or C++ styled CPP code, vetting the nostalgic relation of old peoples with enums, etc., however, there is nothing that an enum class cant offer that enum does, and that too in a more reliable and secure manner.
#include<iostream>
using namespace std;
int main(){
enum Animals: char{BEAR = 12, TIGER = 65};
#ifdef Errors
BEAR = 133; //error enumuerator are constant.
#endif
enum TEAS{RED, BLACK, ORANGE=122}; //Integral literals when not specified a values.
cout << BEAR << " " << TIGER << endl; //implicitly converts to character equivalent of 12 and 65 on the given characterset as supported by implementation locale.
cout << RED << " " << BLACK << " " << ORANGE << endl; //Implicitly converts to the integral literal values.
cout << typeid(BEAR).name() << " " << typeid(TIGER).name(); //??
#ifdef Errors
int TIGER = 133; //error namesapce pollution TIGER is already defined in the given scope.int TIGER = 133; //error namesapce pollution TIGER is already defined in the given scope.cout << r1.a << endl; //error enumerator can't be accessed with an instance of object.
#endif
int r = TIGER; //OK implicitly converts to integral literal value.
char ch = TIGER; //OK implicitly converts to the specified values.
cout << ch << endl;
cout << r << endl;
if(TIGER == 'a'){cout << true;}else{cout << false << endl;} //OK implicitly converts to the given type, while evaluating predicate.
}
Enum class
- An Enum class as mentioned above is tightly scoped to the enumerator itself, and doesn't implicitly converts to the integral types.
- As such though and enumerator can be explicitly converted to the integral type, we can't really assign an enum to any other value without explicit conversion. Conversly speaking though we can assign an enum class value to another enum class value we cant really assign an enum class value to any other integral value without explicit conversion.
- While doing explicit conversion of an integral value to an enum class value we should often take into the consideration the permissble range of an enumerator within enum class type as specified above in the enum section, cause using value outside of the given range for explicit type conversions generally results in a type error.
- Moreover since an enumerator doesn't implicitly converts to an integral value, any operations that are supported on integral values such as bitwise, logical, relational, etc., can't be used with a enum class value without explicit conversion.
- Since an enum class is tightly scopped and doesn't converts to an integral value an enum class value can only be assigned with other enum class values, much like any of other user-defined type.
- Most importantly since an enum class value is tightly scope, it doesn't leak outside of the scope of the enumerator thereby causing namespace pollution, etc. Meaning you can have same declration as an enumerator class's enumerator without leading to any syntax error as woul've been the case with plain enums.
#include<iostream>
#include<math.h>
using namespace std;
enum class TEAS{RED, BLUE, BLACK = 13};
enum class OPS{ADD, SUB, MUL, DIV, MOD, BIT_AND, BIT_XOR, BIT_OR, COMPLIMENT, BIT_RS, BIT_LS, GT, LT, LTEQ, GTEQ, POW, OUT, POST_INC, PRE_INC, POST_DEC, PRE_DEC};
ostream& operator<<(ostream& os, TEAS t1){
os << static_cast<int>(t1);
return os;
}
void do_operations(TEAS& t1, TEAS t2, OPS o1){
int a = static_cast<int>(t1);
int b = static_cast<int>(t2);
switch(o1){
case OPS::ADD: {
int res = a + b;
cout << res << endl;
break;
}
case OPS::SUB: {
int res = a - b;
cout << res << endl;
break;
}
case OPS::MUL: {
int res = a * b;
cout << res << endl;
break;
}
case OPS::DIV: {
int res = a / b;
cout << res << endl;
break;
}
case OPS::MOD: {
int res = a %b;
cout << res << endl;
break;
}
case OPS::BIT_AND: {
int res = a & b;
cout << res << endl;
break;
}
case OPS::BIT_OR: {
int res = a | b;
cout << res << endl;
break;
}
case OPS::BIT_XOR: {
int res = a ^ b;
cout << res << endl;
break;
}
case OPS::BIT_LS: {
int res = (a << b);
cout << res << endl;
break;
}
case OPS::BIT_RS: {
int res = (a >> b);
cout << res << endl;
break;
}
case OPS::COMPLIMENT: {
cout << ~a << endl;
cout << ~b << endl;
}
case OPS::LT: {
cout << (a < b) << endl;
break;
}
case OPS::GT: {
cout << (a > b) << endl;
break;
}
case OPS::LTEQ: {
cout << (a <= b) << endl;
break;
}
case OPS::GTEQ: {
cout << (a >= b) << endl;
break;
}
case OPS::POW: {
cout << pow(a, b) << endl;
break;
}
case OPS::OUT: {
cout << t1;
cout << t2;
}
case OPS::POST_INC: {
cout << a++ << endl;
cout << b++ << endl;
}
case OPS::PRE_INC: {
cout << ++a << endl;
cout << ++b << endl;
}
case OPS::POST_DEC: {
cout << --a << endl;
cout << --b << endl;
}
case OPS::PRE_DEC: {
cout << --a << endl;
cout << --b << endl;
}
}
}
void use_ops(){
TEAS ref = TEAS::RED;
//OK explicitly defined ostream operation on enum class types with conversion to integral literals.
do_operations(ref, TEAS::BLACK, OPS::OUT);
//OK explicitly defined arthematic operation on enum class types with conversion to integral literals.
do_operations(ref, TEAS::BLACK, OPS::ADD);
do_operations(ref, TEAS::BLACK, OPS::SUB);
do_operations(ref, TEAS::BLACK, OPS::MUL);
do_operations(ref, TEAS::BLACK, OPS::DIV);
//OK explicitly defined Bitwise operation on enmerator type with explicit conversion to integral iterals.
do_operations(ref, TEAS::BLACK, OPS::BIT_AND);
do_operations(ref, TEAS::BLACK, OPS::BIT_OR);
do_operations(ref, TEAS::BLACK, OPS::BIT_XOR);
do_operations(ref, TEAS::BLACK, OPS::BIT_LS);
do_operations(ref, TEAS::BLACK, OPS::BIT_RS);
//OK explicitly defined operations for relation operators on enum class Types with explicit conversion to integral literals.
do_operations(ref, TEAS::BLACK, OPS::LT);
do_operations(ref, TEAS::BLACK, OPS::GT);
do_operations(ref, TEAS::BLACK, OPS::LTEQ);
do_operations(ref, TEAS::BLACK, OPS::GTEQ);
//OK explicitly defined operations for rasing to a power operation on enum class type with explicit conversion to integral literals.
do_operations(ref, TEAS::BLACK, OPS::POW);
//OK explicity defined the operator for pre, post inc and decrement operation on enum class operator with explicit type conversion to integral literals
do_operations(ref, TEAS::BLACK, OPS::POST_INC);
do_operations(ref, TEAS::BLACK, OPS::PRE_INC);
do_operations(ref, TEAS::BLACK, OPS::POST_DEC);
do_operations(ref, TEAS::BLACK, OPS::PRE_DEC);
}
int main(){
enum class Animals: char{BEAR = 'a', TIGER = 65};
Animals c1 = Animals::TIGER; //OK can be assigned to enum class type of Animal.
#ifdef Errors
Animals c2 = 12; //error can't implicitly converts to integral value.
cout << c1 << endl; //error no ostream operator defined on object of type Anumals or enum class.
#endif
use_ops();
TEAS c2 = static_cast<TEAS>(12); //OK explicit conversion to integral values.
cout << c2 << endl; //OK Ostream operator defined for enum class type TEAS.
TEAS c3 = static_cast<TEAS>(1e+3); //implementation defined 1e+3 is beyound the permissble range of TEAS enumerator.
TEAS c4 = static_cast<TEAS>(12.3f); //implementation defined 1e-3 is beyound the permissble range of TEAS enumerator. Note since the enum class integral literals we should convert them to floating point literals.
int BEER = 13e+4; //OK enum class are tightly scope and deosn't lead to namespace pollution.
cout << c3 << " " << c4 << endl;
return 0;
}
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! 😁