Programming in C++
Lab 8 (extension lab): Bits 'n bobs
This lab contains some additional features of C++. Many of them are not available in all C++ Versions.
auto/decltype
The auto keyword can be used instead of a type, which will be found by the compiler. Notice this is happining at compile time, C++ is always statically typed. It can be helpful for example with iterators:
#include <bits/stdc++.h> using namespace std; int main() { // Create a set of strings set<string> st; st.insert({ "geeks", "for", "geeks", "org" }); // 'it' evaluates to iterator to set of string // type automatically for (auto it = st.begin(); it != st.end(); it++) cout << *it << " "; return 0; }
Excessive use of auto can make the code hard to read, though.
decltype can be used to derive a type from a variable:
#include <bits/stdc++.h> using namespace std; // Driver Code int main() { int x = 5; // j will be of type int : data type of x decltype(x) j = x + 5; cout << j; return 0; }
Again, this is resolved at compile-time.
Since C++ 14, auto can also be used as a return type of functions (it was possible before, but required extra tricks). The function must return a single type only, though (remeber this is compile time resolution).
typeid
In contrast, typeid can find the type at runtime. Remember polymorphism from last session - how can we find out, what is "really" in a pointer?
#include <iostream> #include <typeinfo> using namespace std; class B { public: // Virtual function virtual void f() { cout << "The base class function is called.\n"; } }; class D: public B { public: void f() { cout << "The derived class function is called.\n"; } }; int main() { B base; D derived; B *basePtr = &base; cout << typeid(*basePtr).name() << endl; basePtr->f(); basePtr = &derived; cout << typeid(*basePtr).name() << endl; basePtr->f(); return 0; }
That makes it possible to write code which only calls a method if the object is of a certain type (but notice the compiler dependency of the name).
File handling
C++ can handle files and read/write from/to files. The most relevant classes for that are:
- ofstream: Stream class to write on files
- ifstream: Stream class to read from files
- fstream: Stream class to both read and write from/to files.
cin and cout, which represent standard input and output, inherit from the same classes as ofstream/ifstream do.
An example for reading and writing could look like this:
/* File Handling with C++ using ifstream & ofstream class object*/ /* To write the Content in File*/ /* Then to read the content of file*/ #include <iostream> /* fstream header file for ifstream, ofstream, fstream classes */ #include <fstream> using namespace std; // Driver Code int main() { // Creation of ofstream class object ofstream fout; string line; // by default ios::out mode, automatically deletes // the content of file. To append the content, open in ios:app // fout.open("sample.txt", ios::app) fout.open("sample.txt"); // Execute a loop If file successfully opened while (fout) { // Read a Line from standard input getline(cin, line); // Press -1 to exit if (line == "-1") break; // Write line in file fout << line << endl; } // Close the File fout.close(); // Creation of ifstream class object to read the file ifstream fin; // by default open mode = ios::in mode fin.open("sample.txt"); // Execute a loop until EOF (End of File) while (getline(fin, line)) { // Print line (read from file) in Console cout << line << endl; } // Close the file fin.close(); return 0; }
We use the open method here. Alternatively, there are constructors which accept a file name and open the file. File paths are relative or absolute. Notice they must comply with platform-dependant naming. The << and >> operators work just like in cout/cin. getline() reads a line, there is also get(), which reads a character. close() is important since the streams are buffered. There are options which can be passed to the open operation. When writing, the file can be opened for appending (app) or truncating (trunc), deleting previous content.
It is also possible to read/write binary data (binary). Notice that the strings we used are pure ASCII strings. In particular that means that characters are 7 bits and written as bytes to the files. If a file is opened as binary, it is possible to control all 8 bits. Of course once a file is written, it is possible to consider it as either binary or text (it is all bits after all), but this can have enexptected effects.
Unicode
C++ can handle Unicode, but it needs special handling as strings as well as files:
#include <iostream> #include <fstream> #include <string> #include <locale> #include <codecvt> #include <fcntl.h> using namespace std; int main() { // Create some strings with Unicode characters wstring ws1 = L"Infinity: \u221E"; wstring ws2 = L"Euro: €"; wchar_t w[] = L"Infinity: \u221E"; wofstream out; out.open("unicode.txt"); const std::locale utf8_locale = std::locale(std::locale(), new std::codecvt_utf8<wchar_t>()); out.imbue(utf8_locale); out << ws2 << endl; out.close(); wcout << ws2 << endl; }
That should work, but of course it leaves the question open which encoding is chosen. Unfortunately, this seems to depend on implementation and is not clearly defined. Looks like utf-8 is used for me. (yes, langauges like Java have a better way to handle things here.)
Serialization
By default, C++ has no built-in serialization (serialization means reading/writing whole objects to/from disk). There are lots of methods given for it, but most/all of them have limitations (no pointers in object etc.). A pretty good overview is https://isocpp.org/wiki/faq/serialization. Boost seems to be a good library.
Enums
C++ has an enum data type, which can be declared as follows:
enum myenum{value1, value2, value3};
It can then be used as:
myenum e; e=value3; cout<<e;
The output here should be 2. This is because internally the enum is converted to integer constants. This also means that a value cannot show up in two enums, and values from enums cannot be used as variable names. Finally, type safety is a problem.
In C++ 11, enum classes solve these problems:
enum class Color { Red, Green, Blue }; Color x = Color::Green;
Modules
We have seen that historically C++ programs are divided into translation units (cpp files, strictly speaking after preprocessing). Includes are then used to make those available somewhere else. In newer C++, modules can take that role. Note this is available from C++ 20, and your compiler may not have it.
Modules have names. They consist of the usual symbols (i. e. letters, numbers, a few special characters) and dots. Dots can serve to divide module names, something like language.finno_ugric.estonian, but this does not involve a hierarchy (same as Java packages). So language.finno_ugric is a different module, just like mymodule is a different module.
A module is made up of one or more source code files compiled into a binary file. The binary file describes all the exported types, functions, and templates in the module. When a source file imports a module, the compiler reads in the binary file that contains the contents of the module. Reading the binary file is much faster than processing a header file. Also, the binary file is reused by the compiler every time the module is imported, saving even more time. Because a module is built once rather than every time it's imported, build time can be reduced, sometimes dramatically. Compared to header files, modules enforce stricter rules on name clashes.
For a module, we need a module interface unit, which has export module in it:
export module speech; export const char* get_phrase() { return "Hello, world!"; }
Here, the implementation is in the same file, but it can be separated. This file has (it seems by convention) the extension .cppm. An implementation would be a cpp file. Both get compiled to .o files.
We can then use this by importing it:
// main.cpp import speech; import <iostream>; int main() { std::cout << get_phrase() << '\n'; }
This would be compiled to an executable together with the .o files of the module (no header file needed). Modules can contain classes as well as functions.
Modules can be divided into partitions:
// speech.cpp export module speech; export import :english; export import :spanish; // speech_english.cpp export module speech:english; export const char* get_phrase_en() { return "Hello, world!"; } // speech_spanish.cpp export module speech:spanish; export const char* get_phrase_es() { return "¡Hola Mundo!"; } // main.cpp import speech; import <iostream>; import <cstdlib>; int main() { if (std::rand() % 2) { std::cout << get_phrase_en() << '\n'; } else { std::cout << get_phrase_es() << '\n'; } }
Notice the colon : instead of the dot . - a dot does not make a partition, it does not really mean something.
It is also possible to divide interfaces from implementations:
// speech.cpp export module speech; import :english; import :spanish; export const char* get_phrase_en(); export const char* get_phrase_es(); // speech_english.cpp module speech:english; const char* get_phrase_en() { return "Hello, world!"; } // speech_spanish.cpp module speech:spanish; const char* get_phrase_es() { return "¡Hola Mundo!"; }
If you are in a "modularized" C++ environment, typically quite a few headers can be replaced by a single module.