Also, const allows us to pass literals and temporaries.
intbyT(int n){
return7*n;
}
intbyCRef(constint & n){
n=n+1;//error n is constreturn2*n;
}
intbyRef(int& n){
n=n+1;//changes the value of parameterreturn2*n;
}
intmain(){
byRef(2);//error cannot bind a non-const lvalue to rvalue
byCRef(2);//OK
byRef(byT(2));//error since the return value of byT is a temp
byCRef(byT(2));//OKint& r=byT(2);//error cannot bind int&& res=byT(2);//ok
}
One important property of references is that they cannot be reassigned.
#include<iostream>intmain(){
int x=19,y=23;
//int &xr;//error a reference must be initializedint& xr=x;
int& yr=y;
/* this does NOT reassign xr to reference y
* it merely assigns the value of y to xr and thus to x
*/
xr=yr;
std::cout<<"value of x="<<x<<"\n";
xr=45;
std::cout<<"value of x="<<x<<"\n";
std::cout<<"value of y="<<y<<"\n";
}
We can prevent modification to a variable we reference to.
For example in the above if we declare const int& xr=x; it means that x cannot be modified using xr (it can still be modified).
In this case the line xr=yr; will generate an error.
Note that in some text they write int const& xr=x;
This is equivalent but the first syntax conveys the fact that the int is unmodifiable not the reference, since by definition a reference is unmodifiable.
As we will see later a class containing a member of type reference cannot be assigned (assignment operator is deleted)
Classes
In C++ new types are created using classes.
Once a class is defined new objects can be instantiated from such a class.
Minimal syntax of a (useless) class
classTest{};
intmain(){
Test t;
}
A class can have member variables and member functions.
classTest{int _x;
public:
int& x(){
return _x;
}
};
intmain(){
Test t;// at this point _x is undefined
t.x()=17;
}
By default all members of a class are private and hence inaccessible from outside the scope of the object.
To make a member accessible we use the keyword public.
Note that the member functionx() returns a reference to _x and this allows us to change the value of _x.
We can use pointers as usual where the code below is equivalent to the one above but using the arrow instead of the dot operator.
intmain(){
Test * p=new Test();
p->x()=17;
}
In fact the private and public qualifiers can be used for any and all members. For example
classTest {private: int _x;//_x is privatepublic: int _y;// _y is publicint _z;// _z is public. The keyword carries over until it changesprivate: voidf(){}
public: int& x(){return _x;}
}
But usually all public members are grouped together using a single keyword and the same for private members;
intmain(){
Test t;
t._x;//error _x is inaccessible
t._y=t._z;//OK both are public
t.x();//OK
t.f();//Error f is inaccessible
}
Constructors and destructors
For builtin types like int and double a variable is "created" (memory is reserved) when the variable is declared.
Once the variable is out of scope is it "destroyed" (memory is released).
The same thing is done for objects instantiated from classes.
This is done by using constructor and destructor.
When we don't supply our own versions a default version is used by the compiler which basically calls the constructors and destructors of the member variables.
classTest {public:
int _x;
double _y;
}
intmain(){
Test t;
std::cout<<t._x<<std::endl;
std::cout<<t._y<<std::endl;
}
No constructor is supplied so the compiler uses a default that creates variables _x and _y.
A constructor builds an object bottom up.
the constructor of the base class (if any) is called
members instructors are called
Finally the constructor body is executed.
For the destructor the opposite happens.
For example
structItem {
Item(){std::cout<<"Item ctor\n";}
Item(int i){std::cout<<"Item ctor with input\n";}
};
structTest {
Item _i;
int x;
};
voidnoinit(){
int x=12,y=77,z=99;
Test t;
std::cout<<t.x<<std::endl;
}
voidinit(){
int x=12,y=77,z=99;
Test t {};//initialize to zerostd::cout<<t.x<<std::endl;
}
intmain(){
noinit();
init();
}
Run to get the output
item ctor
723520304
item ctor
0
As we can see from the above example built-in types are not initiaized
sometimes they are zero sometimes they are not, it depends on the compiler.
For class types the default constructor is called. We can control the constructor and the initialization of members as follows
structTest {
Test(int x,int i):_x(x),_i(i){}
}
As mentioned before, classes containing references or constants have their assignment operator automatically deleted
voiddoit(std::string&& s){
if(s!="hello"){
s="hello"; //this line so we don't go into infinite recursion
doit(std::move(s));
}
}
Move semantics allows us to transfer ownership of resources.
intmain(){
std::string s{"hello there"};
std::string ts=std::move(s);
std::cout<<"the string s is "<<s<<"\n";
std::cout<<"the string ts is "<<ts<<"\n";
std::vector<int> v{1,2,3,4};
std::cout<<"values of v before move are ";
for(auto& x:v)std::cout<<x<<",";
std::vector<int> tv=std::move(v);
std::cout<<"\nafter move they are ";
for(auto& x:v)std::cout<<x<<",";
std::cout<<"\nand the values of tv are ";
for(auto& x:tv)std::cout<<x<<",";
std::cout<<std::endl;
}
you can try it here. We illustrate further with our own, very simple, container.
structContainer {int* p = nullptr;
int _size;
Container(int size) :_size(size), p(newint[size] {}) {}
Container(const Container& rhs) {//copy constructorif (&rhs != this) {
_size = rhs._size;
p = newint[_size];
for (int i = 0; i < _size; ++i)
p[i] = rhs.p[i];
}
}
//usually there is also an assignment operator
Container(Container&& rhs) {
p = rhs.p;
rhs.p = nullptr;
rhs._size = 0;
}
Container& operator=(Container&& rhs) {
if (p)delete[] p;
p = rhs.p;
_size = rhs._size;
rhs._size = 0;
rhs.p = nullptr;
return *this;
}
voidprint(){
for (int i = 0; i < _size; ++i)
std::cout << p[i] << ",";
std::cout << "\n";
}
~Container() {
if (p)delete[] p;
}
};
intmain(){
Container c(10);
std::cout<<"Content of c \n";
c.print();
Container d(std::move(c));
std::cout<<"Content of c \n";
c.print();
Container e(3);
d = std::move(e);
std::cout<<"Content of d \n";
d.print();
}
unless the compiler performs return value optimization (rvo) the following occurs
(in g++ or clang++ specify -fno-elide-constructors to skip optimization)
structTest {
Test(){ std::cout<<"ctor\n";}
Test(const Test& rhs){
std::cout<<"copy ctor\n";
}
~Test(){ std::cout<<"dtor\n";}
};
Test retTest(){return Test();}
intmain(){
Test t=retTest();
std::cout<<"temporary returned by refTest is destroyed\n";
std::cout<<"end of main t will be destroyed\n";
}
what happens is the following
inside function retTest() an object of type Test is created on the stack
a tmp object of type Test is copy constructed from that object
the object on the stack is dtored
t in main is copy ctored from the tmp
tmp is destroyed
when main exists t is destroyed
You can test the code below here. Note the -fno-elide-constructors option in the bottom right of the screen.
$g++-10 -fno-elide-constructors -std=c++11 rvopt.cpp
$./a.out
ctor
copy ctor
dtor
copy ctor
dtor
temporary returned by refTest is destroyed
end of main t will be destroyed
dtor
If we remove the -fno-elide-constructors you get this output. Try it here
$g++-10 -std=c++20 rvopt.cpp
$./a.out
ctor
temporary returned by refTest is destoryed
end of main t will be destroyed
dtor
Note: since c++17 this is no longer possible. Try it here
Pointers
A pointer variable is a variable that holds an address. We say variable p points to variable x if p holds the address of x: int *p=&x;.
intmain(){
int x=17,y=45;
int* p=&x;
std::cout<<p<<std::endl;//prints the value of p, i.e. the address of xstd::cout<<*p<<std::endl;//prints the value store at the location p, i.e. x
*p=23;//change the value of x
p=&y;//p now stores the address of y
}
Pointers usually are used when we need to dynamically allocate memory.
intmain(){
int *p=newint;//reserve space for int. Value undefinedint *q=newint(8);//reserve space for int and store 8
*p=55;//store value 55 at address pdelete p;//release the reserved memory;
}
The new expression can be used with any object. In particular, we can use it to create an array dynamically
intmain(){
constint n=8;
int* p=newint[n];//Note the difference from int(n)/* fill the array with values *//* p IS a variable so we make
* copy before changing it
*/for(int i=0,*q=p;i<n;++i,++q)
*q=i;
for(int i=0;i<n;++i)
std::cout<<p[i]<<",";
std::cout<<"\n";
}
As we mentioned before we can use a pointer to any type.
pointers provide flexibility but they can cause a variety of problems.
One of the problems is shared ownership of a resource.
In order not to have memory leaks, we need to free the allocated memory when done.
This particular problem occurs when two or more pointer variables have the address of the same dynamically allocated resource.
In such a situation we either try to free the memory multiple times, access a resource that no longer exists
or forget to release the memory which causes memory leaks.
We illustrate with the following simple example.
using Long = unsignedlonglong;
void* memory_block(size_t size){
returnoperatornew(size);
}
voidrelease_memory(void* p){
operatordelete(p);
}
structLeaker {int* _values=nullptr;
int _size;
Leaker(Long size) :_size(size) {
_values = (int*)memory_block(_size * sizeof(int));
}
int* values(){
return _values;
}
~Leaker(){//Do we free memory here ?
}
};
intmain(){
constunsignedlonglong n = 1 << 20;
for (int i = 0; i < 50; ++i) {
Leaker x(n);
int* p = x.values();
}
/* keep the program running long enough */int y;
std::cout << "type anything\n";
std::cin >> y;
}
the class Leaker allocates 1MB of memory.
It has a choice, either it does not free the allocated memory, as we did in the example above
or frees it in the destructor with the danger that p will also free it.
Using the performance profiler in VS (Debug->Performance profiler) we see that the above code uses about 200MB
which makes sense since each iteration allocates 4MB.
The other choice is to modify the destructor as follows
~Leaker(){
if(_values!=nullptr)delete _values;
}
This works provided the user (i.e. the code calling int p*=x.value()) does not execute delete p; which crashes the program. You can try that scenario here
To avoid problems like these we use either std::unique_ptr<T> or std::shared_ptr<T>.
The first enforces exclusive
ownership whereas
the second allows shared ownership.
We start with an example of the second.
using Long = unsignedlonglong;
void* memory_block(size_t size){
returnoperatornew(size);
}
voidrelease_memory(void* p){
operatordelete(p);
}
structShared_Owner {std::shared_ptr<int> _values;
Long _size;
Shared_Owner(Long size) :_size(size) {
/* in three steps for clarity */void* raw = memory_block(_size * sizeof(int));
std::shared_ptr<int> q((int*)raw);
_values = q;
/*q will be destroyed here. It is ok
* it holds no resource since it was
* transfered to p
*/
}
std::shared_ptr<int> values(){
return _values;
}
longcount(){
return _values.use_count();
}
};
intmain(){
constunsignedlonglong n = 1 << 20;
Shared_Owner x(n);
{
std::shared_ptr<int> p = x.values();
std::cout<<"after p is created="<<x.count()<<"\n";
std::shared_ptr<int> q=x.values();
std::cout<<"after q is created="<<x.count()<<"\n";
}
std::cout<<"count outside block= "<<x.count()<<"\n";
}
there is no delete of a std::shared_ptr - - it is automatically destroyed (i.e. dtor is called) when it goes out of scope and
it frees the resource it is pointing to only if it is the last shared_ptr copy.
it is used exactly like pointers.
In the example above there are three std::shared_ptr,
all pointing to the resource allocated by the Shared_Owner object x.
Inside the block two std::shared_ptr objects are created p and q,
both pointing to the memory block allocated by x. - When they go out of scope, and therefore they are destroyed,
the allocated memory is not freed, the number of copies is just decremented.
when x goes out of scope, it is the last shared_ptr pointing to the resource
therefore it frees it.
-You can try the above example here here.
The std::unique_ptr<T> is similar except it enforces exclusive ownership.
Below is a similar example illustrating the memory automatically released by the std::unique_ptr when it is destroyed.
Note the two changes due to noncopiable nature of std::unique_ptr.
First, in function mod the std::unique_ptr must be passed and returned by reference
we cannot make a copy.
in the statement std::unique_ptr<int> t = std::move(mod(p->res)); the resource held by p is transferred to t.
using Long = unsignedlonglong;
void* memory_block(size_t size){
returnoperatornew(size);
}
voidrelease_memory(void* p){
operatordelete(p);
}
structUnique_Owner {std::unique_ptr<int> _values;
Long _size;
Unique_Owner(Long size) :_size(size) {
/* in three steps for clarity */void* raw = memory_block(_size * sizeof(int));
std::unique_ptr<int> q((int *)raw);
_values = std::move(q);
/*q will be destroyed here. It is ok
* it holds no resource since it was
* transfered to p
*/
}
std::unique_ptr<int> values(){
returnstd::move(_values);
}
};
intmain(){
constunsignedlonglong n = 1 << 20;
for (int i = 0; i < 50; ++i) {
Unique_Owner x(n);
std::unique_ptr<int> p = x.values();
std::this_thread::sleep_for(std::chrono::microseconds(10));
}
}
Using the performance profiler in VS (Debug->Performance profiler)
the above code uses only 4MB
which means each iteration 4MB are allocated and then freed.
Templates
On many occasions we write multiple versions of the same code to handle different types. For example suppose we want to write a function to add two numbers (using the + operator) we write
Recall also that the + operator can be used to concatenate strings so we have to add that also. Since the all of those versions only the type changes, c++ allows us to pass the type as a parameters using templates.
#include<iostream>#include<string>template<typename T>
T add(T x,T y){
return x+y;
}
intmain(){
int x=2,y=3;
double u=3.4,v=3;
std::string s="hello",k="there";
std::cout<<add(x,y)<<std::endl;
std::cout<<add(u,v)<<std::endl;
std::cout<<add(s,k)<<std::endl;
}
In the above example the compiler automatically deduces the type which sometimes it cannot and we have to specify it as follows:
intmain(){
std::vector<int> v {7,2,4,2,2,8,2};
int r=std::count(v.begin(),v.end(),2);
std::cout<<" number of 2's is "<<r<<"\n";
int even=std::count_if(v.begin(),v.end(),is_even);
int odd=std::count_if(v.begin(),v.end(),is_odd());
std::cout<<" number of even is "<<even<<" and odd is "<<odd<<"\n";
}
count_if takes a unary predicate as a third parameter and this can be either a function pointer or a function object.
Recall, a function object is one that defines an operator().
Find the first occurrence of an object in a container and returns an iterator pointing to it.
intmain(){
std::vector<int> v {3,7,9,1,1,8,19,2,1,4};
auto first=std::find(v.begin(),v.end(),1);
/* print all the elements from first
* occurrence of the value 1 to the end
*/for(auto itr=first;itr!=v.end();++itr)
std::cout<<*itr<<",";
std::cout<<"\n";
first=std::find_if(v.begin(),v.end(),
[](int x){return x%2==0;});
/* all the elements from the first
* occurrence of an even number to
* the end
*/for(auto itr=first;itr!=v.end();++itr)
std::cout<<*itr<<",";
std::cout<<"\n";
}
The STL functions std::remove and std::remove_if do not remove anything.
They just partition the input range into two parts
a left part which contains the original objects, in the same order, where the desired object is removed,
a right part which contains "non useful" objects: either the one to be removed or additional copies of the already existing objects.
intmain(){
std::vector<int> v {1,2,3,4,2,5,2,6};
auto new_end=std::remove(v.begin(),v.end(),2);
for(auto itr=v.begin();itr!=new_end;++itr)
std::cout<<*itr<<",";
std::cout<<"\n";
/* "non useful" part */for(auto itr=new_end;itr!=v.end();++itr)
std::cout<<*itr<<",";
std::cout<<"\n";
}
std::remove_if works the same way but using a predicate instead of a value.
These two functions are useful, especially, for the remove-erase idiom.
Once we have the "useful" range with the desired object removed from it we can actually erase it.
intmain(){
std::vector<int> v {1,2,3,4,2,5,2,6};
auto new_end=std::remove(v.begin(),v.end(),2);
v.erase(new_end,v.end());
/* the vector contains only
* the "useful" part
*/for(auto x:v)
std::cout<<x<<",";
std::cout<<"\n";
}
Sometimes we need to print all the values in a container using a range-based for loop and auto.
We can type a little less if we defined an operator << that can handle containers.
template<typename Ostream,typename Container>
Ostream& operator<<(Ostream& os,Container& c) {
for (auto x : c)
os<< x << ",";
return os;
}
The above works well with all sorts of containers, except with strings
In that case it will print it as characters separated by commas.
We can fix this by using template specialization
template <>
std::ostream& operator<< <std::ostream, std::string>
(std::ostream& os, std::string& s) {
/* cannot use os<<s because we enter infinite recursion */
os.write(s.c_str(), s.size());
return os;
}
substrings and subranges
The std::string class has a convenient member std::string::find
it returns the position of the first character of the substring in the string if found
and std::string::npos otherwise.
A general "substring" finding method is std::search which will find a "subrange" in any container, including strings.
intmain(){
std::string s {"First Middle Last Middle"};
std::string sub {"Middle"};
auto pos=s.find(sub);
if(pos!=std::string::npos)std::cout<<"found at "<<pos<<"\n";
elsestd::cout<<"not found\n";
std::vector<int> v{4,2,7,3,1,5};
std::vector<int> needle {7,3,1};
/* returns an iterator to the beginning of the match
* or v.end() if no match is found
*/auto itr=std::search(v.begin(),v.end(),needle.begin(),needle.end());
}
Lambda expressions
As we saw, many algorithms, especially in STL, take a callable object as a parameter.
Lambda expressions are a convenient way to define such a callable object.
We rewrite the previous example using lambda expressions.
intmain(){
std::vector<int> v {7,2,4,2,2,8,2};
int r=std::count(v.begin(),v.end(),2);
std::cout<<" number of 2's is "<<r<<"\n";
int even=std::count_if(v.begin(),v.end(),
[](int x){return x%2==0;});
int odd=std::count_if(v.begin(),v.end(),
[](int x){return x%2!=0;});
std::cout<<" number of even is "<<even<<" and odd is "<<odd<<"\n";
//counting the number of 2's in a different way// just to introduce captureint val=2;
std::count_if(v.begin(),v.end(),
[val](int x){return x==val;});
}