100% found this document useful (1 vote)
31 views27 pages

Programming in C and C++ - Unit V

Programming in c and c++ unit v notes

Uploaded by

yeswanth3604
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
Download as pdf or txt
100% found this document useful (1 vote)
31 views27 pages

Programming in C and C++ - Unit V

Programming in c and c++ unit v notes

Uploaded by

yeswanth3604
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
Download as pdf or txt
Download as pdf or txt
You are on page 1/ 27

UNIT V

PROGRAMMING IN CAND C++ - SCSA1202

1
I. TEMPLATES AND EXCEPTION HANDLING

Function Templates and Class Templates – Name Spaces – Standard Template Library –
Casting – Exception Handling – Case Study.
C++ Templates:
A C++ template is a powerful feature added to C++. It allows you to define the generic
classes and generic functions and thus provides support for generic programming. Generic
programming is a technique where generic types are used as parameters in algorithms so that
they can work for a variety of data types.
Templates can be represented in two ways:
• Function templates
• Class templates

Fig.5.1.Templates in C++
Function Templates:
We can define a template for a function. For example, if we have an add() function, we can
create versions of the add function for adding the int, float or double type values.
Function Template
Generic functions use the concept of a function template. Generic functions define a set of
operations that can be applied to the various types of data.
The type of the data that the function will operate on depends on the type of the data passed
as a parameter.
For example, Quick sorting algorithm is implemented using a generic function, it can be
implemented to an array of integers or array of floats.
A Generic function is created by using the keyword template. The template defines what
function will do.
Syntax of Function Template
template < class Ttype> ret_type func_name(parameter_list)
{

2
// body of function.
}
Where Ttype: It is a placeholder name for a data type used by the function. It is used within
the function definition. It is only a placeholder that the compiler will automatically replace
this placeholder with the actual data type.
class: A class keyword is used to specify a generic type in a template declaration.
Let's see a simple example of a function template:
#include <iostream>
using namespace std;
template<class T> T add(T &a,T &b)
{
T result = a+b;
return result;

}
int main()
{
int i =2;
int j =3;
float m = 2.3;
float n = 1.2;
cout<<"Addition of i and j is :"<<add(i,j);
cout<<'\n';
cout<<"Addition of m and n is :"<<add(m,n);
return 0;
}
Output:
Addition of i and j is :5
Addition of m and n is :3.5
In the above example, we create the function template which can perform the addition
operation on any type either it can be integer, float or double.

3
Function Templates with Multiple Parameters
We can use more than one generic type in the template function by using the comma to
separate the list.
Syntax
template<class T1, class T2,.....>
return_type function_name (arguments of type T1, T2....)
{
// body of function.
}
In the above syntax, we have seen that the template function can accept any number of
arguments of a different type.
Let's see a simple example:
#include <iostream>
using namespace std;
template<class X,class Y> void fun(X a,Y b)
{
std::cout << "Value of a is : " <<a<< std::endl;
std::cout << "Value of b is : " <<b<< std::endl;
}
int main()
{
fun(15,12.3);
return 0;
}
Output:
Value of a is : 15
Value of b is : 12.3
In the above example, we use two generic types in the template function, i.e., X and Y.
Overloading a Function Template
We can overload the generic function means that the overloaded template functions can differ
in the parameter list.

4
Let's understand this through a simple example:
#include <iostream>
using namespace std;
template<class X> void fun(X a)
{
std::cout << "Value of a is : " <<a<< std::endl;
}
template<class X,class Y> void fun(X b ,Y c)
{
std::cout << "Value of b is : " <<b<< std::endl;
std::cout << "Value of c is : " <<c<< std::endl;
}
int main()
{
fun(10);
fun(20,30.5);
return 0;
}
Output:
Value of a is : 10
Value of b is : 20
Value of c is : 30.5
In the above example, template of fun() function is overloaded.
CLASS TEMPLATE
Class Template can also be defined similarly to the Function Template. When a class uses the
concept of Template, then the class is known as generic class.
Syntax
template<class Ttype>
class class_name
{
.

5
.
}
Ttype is a placeholder name which will be determined when the class is instantiated. We can
define more than one generic data type using a comma-separated list. The Ttype can be used
inside the class body.
Now, we create an instance of a class
class_name<type> ob;
where class_name: It is the name of the class.
type: It is the type of the data that the class is operating on.
ob: It is the name of the object.
Let's see a simple example:
#include <iostream>
using namespace std;
template<class T>
class A
{
public:
T num1 = 5;
T num2 = 6;
void add()
{
std::cout << "Addition of num1 and num2 : " << num1+num2<<std::endl;
}

};
int main()
{
A<int> d;
d.add();
return 0;
}

6
Output:
Addition of num1 and num2 : 11

In the above example, we create a template for class A. Inside the main() method, we create
the instance of class A named as, 'd'.

CLASS TEMPLATE WITH MULTIPLE PARAMETERS


We can use more than one generic data type in a class template, and each generic data type is
separated by the comma.
Syntax
template<class T1, class T2, ......>
class class_name
{
// Body of the class.
}
Let's see a simple example when class template contains two generic data types.

#include <iostream>
using namespace std;
template<class T1, class T2>
class A
{
T1 a;
T2 b;
public:
A(T1 x,T2 y)
{
a = x;
b = y;
}
void display()

7
{
std::cout << "Values of a and b are : " << a<<" ,"<<b<<std::endl;
}
};
int main()
{
A<int,float> d(5,6.5);
d.display();
return 0;
}
Output:
Values of a and b are : 5,6.5
C++ Namespaces
Namespaces in C++ are used to organize too many classes so that it can be easy to handle the
application.
For accessing the class of a namespace, we need to use namespacename::classname. We can
use using keyword so that we don't have to use complete name all the time.
In C++, global namespace is the root namespace. The global::std will always refer to the
namespace "std" of C++ Framework.
C++ namespace Example
Let's see the simple example of namespace which include variable and functions.
#include <iostream>
using namespace std;
namespace First {
void sayHello() {
cout<<"Hello First Namespace"<<endl;
}
}
namespace Second {
void sayHello() {
cout<<"Hello Second Namespace"<<endl;

8
}
}
int main()
{
First::sayHello();
Second::sayHello();
return 0;
}
Output:
Hello First Namespace
Hello Second Namespace
C++ namespace example: by using keyword
Let's see another example of namespace where we are using "using" keyword so that we don't
have to use complete name for accessing a namespace program.
#include <iostream>
using namespace std;
namespace First{
void sayHello(){
cout << "Hello First Namespace" << endl;
}
}
namespace Second{
void sayHello(){
cout << "Hello Second Namespace" << endl;
}
}
using namespace First;
int main () {
sayHello();
return 0;
}

9
Output:
Hello First Namespace
Standard Template Library
STL is an acronym for standard template library. It is a set of C++ template classes that
provide generic classes and function that can be used to implement data structures and
algorithms. It is basically a generic library i.e. a single method/class can operate on different
data types. So, as understood, we won’t have to declare and define the same methods/classes
for different data types. Thus, STL saves a lot of effort, reduces the redundancy of the code
and therefore, leads to the increased optimization of the code blocks.
STL is mainly composed of:
Algorithms
Containers
Iterators

Fig.5.1 Standard Template Library


STL provides numerous containers and algorithms which are very useful in completive
programming, for example you can very easily define a linked list in a single statement by
using list container of container library in STL, saving your time and effort.
STL is a generic library, i.e a same container or algorithm can be operated on any data types,
you don’t have to define the same algorithm for different type of elements.

10
For example, sort algorithm will sort the elements in the given range irrespective of their data
type, we don’t have to implement different sort algorithm for different datatypes.
Containers:
Containers Library in STL gives us the Containers, which in simplest words, can be described
as the objects used to contain data or rather collection of object. Containers help us to
implement and replicate simple and complex data structures very easily like arrays, list, trees,
associative arrays and many more.
The containers are implemented as generic class templates, means that a container can be
used to hold different kind of objects and they are dynamic in nature!
Following are some common containers:
vector: replicates arrays
queue: replicates queues
stack: replicates stack
priority_queue: replicates heaps
list: replicates linked list
set: replicates trees
map: associative arrays
Classification of Containers in STL
Containers are classified into four categories:
Sequence containers: Used to implement data structures that are sequential in nature like
arrays(array) and linked list(list).
Associative containers: Used to implement sorted data structures such as map, set etc.
Unordered associative containers: Used to implement unsorted data structures.
Containers adaptors: Used to provide different interface to the sequence containers.
Overview of Iterators in C++ STL
As we have discussed earlier, Iterators are used to point to the containers in STL, because of
iterators it is possible for an algorithm to manipulate different types of data
structures/Containers.
Algorithms in STL don’t work on containers, instead they work on iterators, they manipulate
the data pointed by the iterators. Thus, it doesn’'t matter what is the type of the container and
because of this an algorithm will work for any type of element and we don't have to define
same algorithm for different types of containers.

11
Fig.5.2 Iterators in C++
The above diagram shows to iterators i and j, pointing to the beginning and the end of a
vector.
Defining an Iterator in STL
Syntax for defining an iterator is :
container_type <parameter_list>::iterator iterator_name;
Let's see an example for understanding iterators in a better way:
#include<iostream>
#include<vector>
using namespace std;
int main()
{
vector<int>::iterator i;
/* create an iterator named i to a vector of integers */
vector<string>::iterator j;
/* create an iterator named j to a vector of strings */
list<int>::iterator k;
/* create an iterator named k to a vector of integers */
map<int, int>::iterator l;
/* create an iterator named l to a map of integers */
}
Iterators can be used to traverse the container, and we can de-reference the iterator to get the
value of the element it is pointing to. Here is an example:
#include<iostream>
#include<vector>
int main()

12
{
vector<int> v(10);
/* creates an vector v : 0,0,0,0,0,0,0,0,0,0 */
vector<int>::iterator i;
for(i = v.begin(); i! = v.end(); i++)
cout << *i <<" ";
/* in the above for loop iterator I iterates though the vector v and *operator is used of
printing the element pointed by it. */
return 0;
}
Operations on Iterators in STL
Following are the operations that can be used with Iterators to perform various actions.
advance
distance
next
prev
begin
end
advance() Operation
It will increment the iterator i by the value of the distance. If the value of distance is negative,
then iterator will be decremented.
SYNTAX: advance(iterator i ,int distance)
#include<iostream>
#include<vector>
int main()
{
vector<int> v(10) ; // create a vector of 10 0's
vector<int>::iterator i; // defines an iterator i to the vector of integers
i = v.begin();
/* i now points to the beginning of the vector v */
advance(i,5);

13
/* i now points to the fifth element form the
beginning of the vector v */
advance(i,-1);
/* i now points to the fourth element from the
beginning of the vector */
}
distance() Operation
It will return the number of elements or we can say distance between the first and the last
iterator.
SYNTAX: distance(iterator first, iterator last)
#include<iostream>
#include<vector>
int main()
{
vector<int> v(10) ; // create a vector of 10 0's
vector<int>::iterator i, j; // defines iterators i,j to the vector of integers
i = v.begin();
/* i now points to the beginning of the vector v */
j = v.end();
/* j now points to the end() of the vector v */
cout << distance(i,j) << endl;
/* prints 10 , */
}
next() Operation
It will return the nth iterator to i, i.e iterator pointing to the nth element from the element
pointed by i.
SYNTAX: next(iterator i ,int n)
prev() Operation
It will return the nth predecessor to i, i.e iterator pointing to the nth predecessor element from
the element pointed by i.
SYNTAX: prev(iterator i, int n)

14
begin() Operation
This method returns an iterator to the start of the given container.
SYNTAX: begin()
end() Operation
This method returns an iterator to the end of the given container.
SYNTAX: end()
Algorithms in Standard Template Library
Standard Template Library provides us with different generic algorithms. These algorithms
contain built in generic functions which can be directly accessed in the program.
The Algorithm and its functions can be accessed with the help of Iterators only.
Types of Algorithms offered by Standard Template Library:
Sorting Algorithms
Search algorithms
Non-modifying algorithms
Modifying algorithms
Numeric algorithms
Minimum and Maximum operation

Sorting Algorithm in Standard Template Library

Standard Template Library provides built-in sort() method to sort the elements in a particular
data structure. Internally, it uses a combination of Quick Sort, Heap Sort, and Insertion Sort
to sort the elements.

Syntax:
sort(starting index, end_element_index)

Example:

#include <iostream>
#include <algorithm>

using namespace std;

int main()
{
int arr[5]= {10, 50, 0, -1, 48};

15
cout << "\nArray before sorting:\n";

for(int i = 0; i < 5; ++i)


cout << arr[i] << " ";
sort(arr, arr+5);
cout << "\nArray after sorting:\n";
for(int i = 0; i < 5; ++i)
cout << arr[i] << " ";
return 0;

}
Output:

Array before sorting:


10 50 0 -1 48
Array after sorting:
-1 0 10 48 50

Understanding C++ Casts


A type cast is basically a conversion from one type to another. There are two types of type
conversion:
Type Conversion In C++
Type Conversion refers to conversion from one type to another. The main idea behind type
conversion is to make variable of one type compatible with variable of another type to
perform an operation. For example, to find the sum of two variables, one of int type & other
of float type. So, you need to type cast int variable to float to make them both float type for
finding the sum. In this blog we will learn how to perform type conversion in C++.
In C++, there are two types of type conversion
implicit type conversion & explicit type conversion.
Implicit Type Conversion
Implicit type conversion or automatic type conversion is done by the compiler on its own.
There is no external trigger required by the user to typecast a variable from one type to
another.
This occurs when an expression contains variables of more than one type. So, in those
scenarios automatic type conversion takes place to avoid loss of data. In automatic type
conversion, all the data types present in the expression are converted to data type of the
variable with the largest data type.
Below is the order of the automatic type conversion. You can also say, smallest to largest
data type for type conversion.
bool -> char -> short int -> int -> unsigned int -> long -> unsigned -> long long -> float ->
double -> long double

16
Implicit conversions can lose information such as signs can be lost when signed type is
implicitly converted to unsigned type and overflow can occur when long is implicitly
converted to float.
Now let us look at an example to understand how implicit type conversion works in C++.

Example
#include <iostream>
using namespace std;
int main() 12w
{
int int1 = 100; // integer int1
char char1 = 'c'; // character char1
// char1 implicitly converted to int using ASCII value of 'c' i.e. 99
int1 = int1 + char1;
// int1 is implicitly converted to float
float flt1 = int1 + 2.7;
cout << "int1 = " << int1 << endl
<< "char1 = " << char1 << endl
<< "flt1 = " << flt1 << endl;
return 0;
}
Output
int1 = 199
char1 = c
flt1 = 201.7

Explicit Type Conversion


Explicit type conversion or type casting is user defined type conversion. In explicit type
conversion, the user converts one type of variable to another type. Explicit type conversion
can be done in two ways in C++:
Converting by assignment
Conversion using Cast operator

17
Converting by assignment
In this type conversion the required type is explicitly defined in front of the expression in
parenthesis. Data loss happens in explicit type casting. It is considered as forceful casting.
Let’s look at an example.
Example

#include <iostream>
using namespace std;
int main()
{
double dbl1 = 8.9;
// Explicit conversion from double to int
int res = (int)dbl1 + 1;
cout << "Result = " << res;
return 0;
}
Output
Result = 9
Conversion using Cast Operator
Cast operator is an unary operator which forces one data type to be converted into another
data type. There are four types of casting in C++, i.e. Static Cast, Dynamic Cast, Const Cast
and Reinterpret Cast.
Static Cast – This is the simplest type of cast which can be used. It not only performs
upcasts, but also downcasts. It is a compile time cast. Checks are not performed during the
runtime to guarantee that an object being converted is a full object of the destination type.
Dynamic Cast – It ensures that a result of the type conversion points to the valid, complete
object of the destination pointer type.
Const Cast – manipulates that whether the object needs to be constant or non-constant. It
ensures that either the constant needs to be set or to be removed.
Reinterpret Cast – converts any pointer type to any other pointer type, even of unrelated
classes. It does not check if the pointer type and data pointed by the pointer is same or not.
Let’s look at an example of static cast,
Example
#include <iostream>
18
using namespace std;
int main()
{
float flt = 30.11;
// using cast operator
int int1 = static_cast<int>(flt);
cout <<int1;
}
Output
30
static_cast
static_cast is the main workhorse in our C++ casting world. static_cast handles implicit
conversions between types (e.g. integral type conversion, any pointer type to void*).
static_cast can also call explicit conversion functions.
int * y = static_cast<int*>(malloc(10));
We will primarily use it for converting in places where implicit conversions fail, such as
malloc. There are no runtime checks performed for static_cast conversions. static_cast cannot
cast away const or volatile.
reinterpret_cast
reinterpret_cast is a compiler directive which tells the compiler to treat the current type as a
new type. You can use reinterpret_cast to cast any pointer or integral type to any other
pointer or integral type. This can lead to dangerous situations: nothing will stop you from
converting an int to a std::string *.
You will use reinterpret_cast in your embedded systems. A common scenario where
reinterpret_cast applies is converting between uintptr_t and an actual pointer or between:
error: static_cast from 'int *' to 'uintptr_t'
(aka 'unsigned long') is not allowed
uintptr_t ptr = static_cast<uintptr_t>(p);
1 error generated.
Instead, use this:
uintptr_t ptr = reinterpret_cast<uintptr_t>(p);
reinterpret_cast cannot cast away const or volatile.
const_cast

19
const_cast adds or removes const from a variable. Strangely enough, you can also use
const_cast to add or remove volatile from a variable. No other C++ cast can add or remove
these keywords.
Use this cast carefully. If you declared the original variable as const, it’s not safe to remove
const and start modifying the underlying data.

You should primarily use const_cast to add const to a variable (such as for a function
overload to use a const version). If you need to remove const from a variable, I recommend
stopping and thinking about why you are in this situation.
const_cast and volatile
Removing volatile from a keyword is definitely more common in embedded systems than
removing const. If you decide to use the volatile variable as a function parameter, you will
need to remove the keyword.
Consider the following contrived example:
void test(int * x)
{
//do something
}
int main(void)
{
volatile int p = 0;
test(&p);
return 0;
}
In C, this example would compile. C++ throws an error:
test.c:14:2: error: no matching function for call to 'test'
test(&p);
^~~~
test.c:5:6: note: candidate function not viable: 1st argument ('volatile int *')
would lose volatile qualifier
void test(int * x)
^
1 error generated.

20
To prevent this error, we will need to use const_cast:
test(const_cast<int*>(&p));
dynamic_cast
I have never used dynamic_cast in my embedded projects, and I usually keep run-time-type-
information (RTTI) disabled. I will provide a cursory overview, but please see “Further
Reading” below for more detailed information.
We use dynamic_cast to handle polymorphism. dynamic_cast can convert pointers and
references to any polymorphic type at run-time, primarily to cast down a type’s inheritance
hierarchy. If dynamic_cast can’t find the desired type in the inheritance hierarchy, it will
return nullptr for pointers or throw a std::bad_cast exception for references.
C++ Exception Handling
An exception is a problem that arises during the execution of a program. A C++ exception is
a response to an exceptional circumstance that arises while a program is running, such as an
attempt to divide by zero.
Exceptions provide a way to transfer control from one part of a program to another. C++
exception handling is built upon three keywords: try, catch, and throw.
throw − A program throws an exception when a problem shows up. This is done using a
throw keyword.
catch − A program catches an exception with an exception handler at the place in a program
where you want to handle the problem. The catch keyword indicates the catching of an
exception.
try − A try block identifies a block of code for which particular exceptions will be activated.
It's followed by one or more catch blocks.
Assuming a block will raise an exception, a method catches an exception using a combination
of the try and catch keywords. A try/catch block is placed around the code that might
generate an exception.
Code within a try/catch block is referred to as protected code, and the syntax for using
try/catch as follows −
try {
// protected code
} catch( ExceptionName e1 ) {
// catch block
} catch( ExceptionName e2 ) {
// catch block
} catch( ExceptionName eN ) {
// catch block

21
}
You can list down multiple catch statements to catch different type of exceptions in case your
try block raises more than one exception in different situations.

Throwing Exceptions
Exceptions can be thrown anywhere within a code block using throw statement. The operand
of the throw statement determines a type for the exception and can be any expression and the
type of the result of the expression determines the type of exception thrown.
Following is an example of throwing an exception when dividing by zero condition occurs −
double division(int a, int b) {
if( b == 0 ) {
throw "Division by zero condition!";
}
return (a/b);
}
Catching Exceptions
The catch block following the try block catches any exception. You can specify what type of
exception you want to catch and this is determined by the exception declaration that appears
in parentheses following the keyword catch.
try {
// protected code
} catch( ExceptionName e ) {
// code to handle ExceptionName exception
}
Above code will catch an exception of ExceptionName type. If you want to specify that a
catch block should handle any type of exception that is thrown in a try block, you must put an
ellipsis, ..., between the parentheses enclosing the exception declaration as follows −
try {
// protected code
} catch(...) {
// code to handle any exception
}

22
The following is an example, which throws a division by zero exception and we catch it in
catch block.
Live Demo
#include <iostream>
using namespace std;
double division(int a, int b) {
if( b == 0 ) {
throw "Division by zero condition!";
}
return (a/b);
}
int main () {
int x = 50;
int y = 0;
double z = 0;
try {
z = division(x, y);
cout << z << endl;
} catch (const char* msg) {
cerr << msg << endl;
}
return 0;
}
Because we are raising an exception of type const char*, so while catching this exception, we
have to use const char* in catch block. If we compile and run above code, this would produce
the following result −
Division by zero condition!
C++ Standard Exceptions
C++ provides a list of standard exceptions defined in <exception> which we can use in our
programs. These are arranged in a parent-child class hierarchy shown below −
C++ Exceptions Hierarchy
Here is the small description of each exception mentioned in the above hierarchy −

23
std::exception
An exception and parent class of all the standard C++ exceptions.

std::bad_alloc
This can be thrown by new.
std::bad_cast
This can be thrown by dynamic_cast.
std::bad_exception
This is useful device to handle unexpected exceptions in a C++ program.
std::bad_typeid
This can be thrown by typeid.
std::logic_error
An exception that theoretically can be detected by reading the code.
std::domain_error
This is an exception thrown when a mathematically invalid domain is used.
std::invalid_argument
This is thrown due to invalid arguments.
std::length_error
This is thrown when a too big std::string is created.
std::out_of_range
This can be thrown by the 'at' method, for example a std::vector and
std::bitset<>::operator[]().
std::runtime_error
An exception that theoretically cannot be detected by reading the code.
std::overflow_error
This is thrown if a mathematical overflow occurs.
std::range_error
This is occurred when you try to store a value which is out of range.
std::underflow_error
This is thrown if a mathematical underflow occurs.

24
Lesson Overview & Knowledge Required
In this lesson you will create classes and functions within those classes; some functions will
be overridden by derived classes. To complete this lesson, you should have an understanding
of object-oriented programming and the concepts of encapsulation and polymorphism. You
should be able to build a working class in C++ and add members and functions to the class.
A Case Study - Program Code
The program code you will write will calculate the volume of various containers. A base
class, Cylinder, will be created, with its derived classes, also called child classes or sub-
classes.
First, let's create a C++ program with a Cylinder class. Note that we created a constant for pi
since we will need this for any non-square containers. We are going to use protected for the
members since they can only be used by this class and its children. Finally, a public function
is created that sets the volume. Since radius and height are commonly used to get volume,
this parent/base class will start with those.
#include <iostream>
using namespace std;
const float PI = 3.1415927;
class Cylinder {
// The formula is: V = pi * (r^2) * h
protected:
float radius;
float height;
float volume;
public:
void set_volume (float r, float h) {
radius = r;
height = h;
volume = (PI * (radius * radius) * height) ;
cout << "Base volume is " << volume << endl;
}
};
Next, we'll create a derived, or child class for Cylinder. This one will be for a Cone. The
same function, with the same parameters, is used. However, the formula is different for a
cone.

25
class Cone : public Cylinder {
// The formula is: V = (1/3) * pi * (r^2) * h
public:
void set_volume(float r, float h) {
radius = r;
height = h;
volume = (PI * radius * radius * height) / 3 ;
cout << "Cone volume is " << volume << endl;
}
};
Finally, in the main function, we will create an instance of Cone and an instance of Cylinder.
In each case, call the set_volume function, passing the same parameters. Since the function is
overridden in the derived class, you will get different results.
int main() {
// Create instances of the base and derived classes
Cylinder cylinder;
Cone cone;
// Set the volume of the cylinder
cylinder.set_volume(3, 4);
// Set the volume of the cone
cone.set_volume(3, 4);
return 0;
}
When run, the output should be:
Cylinder volume is 113.097
Cone volume is 37.6991

26
Text / Reference Books:

27

You might also like