Deep Dive into Abstraction and Encapsulation in C++(Part 3)

Lokesh Bihani
5 min readApr 18, 2024

--

This is the third part of my 6-part series on object-oriented programming in C++. In this part I’ll discuss Encapsulation, Abstraction and Type conversions in detail.

Data Hiding

Data Hiding refers to the concept of hiding the inner structure of a class. It is achieved using Encapsulation and Abstraction.

Encapsulation

Encapsulation refers to the bundling of data (attributes or properties) and methods (functions or operations) that operate on the data into a single unit called a class.

In practice, encapsulation is implemented by making all the data members of a class private and providing public getter and setter methods to access and modify those members.

In the iterative process of class design, the principle of encapsulation guides decisions about which data members should be exposed through the public interface of the class. The general rule is to keep data members private initially and selectively provide access as needed, following the principle of least privilege.

By encapsulating data within a class and exposing only a controlled interface to interact with that data, you can prevent unintended access and manipulation, thus enhancing the reliability and stability of the codebase. It also simplifies debugging and maintenance because the access to the data members is localized using encapsulation.

Abstraction

Abstraction hides complex implementation details and exposes only the necessary features to the user.

Abstraction, and Encapsulation always go together. Sometimes, you understand these terms theoretically but struggle to apply them at the code level. Consider the following example to understand this concept better:

Assume starting an engine involves multiple steps like checking fuel level, oil level, igniting spark plugs and finally starting the motor. Everytime a user wants to start the car, they’d have to perform all these steps. This is a nightmare.

Example 1 clearly violates abstraction principle because as a user, I don’t need to know all the steps performed before starting the engine and I certainly shouldn’t be expected to remember the order of steps to be performed to start the engine. However, this class still follows encapsulation because it clearly provides methods in its public interface to interact with the private data members.

In order to fix the issue, you can simply provide a startEngine method and hide all the steps required to start an engine from the user. (Example 2).

// ❌ Violates abstraction principle - Example 1
class Car {
public:
void checkFuelLevel() { /* Implementation */ }
void checkOilLevel() { /* Implementation */ }
void igniteSparkPlugs() { /* Implementation */ }
void engageStarterMotor() { /* Implementation */ }

Car(): fuelLevel(true), oilLevel(true) { }
private:
bool fuelLevel, oilLevel, sparkPlug, motor;
// Other private methods as needed
};

// ===================================================
// ✅ Follows abstraction principle - Example 2
class Car {
public:
void startEngine() {
// Perform multiple steps to start the engine
checkFuelLevel();
checkOilLevel();
igniteSparkPlugs();
engageStarterMotor();
// Other steps as needed
std::cout << "Engine started successfully!" << std::endl;
}
private:
void checkFuelLevel() { /* Implementation */ }
void checkOilLevel() { /* Implementation */ }
void igniteSparkPlugs() { /* Implementation */ }
void engageStarterMotor() { /* Implementation */ }
// Other private methods as needed
};

Now, putting it all together, “Data Hiding” is achieved by providing controlled access to the class (encapsulation) and hiding unnecessary details(abstraction) from the user of the class.

This is what “hiding the inner structure of a class” actually means.

Type conversions in C++

There 4 types of type conversions and I’ll be discussing each one of them one by one.

Primitive type to Class type (Primitive to Class)

  • This involves converting a primitive data type, such as int, double, or char, to an object of a class type.
  • You can define constructors in your class that accept primitive data types as parameters to facilitate this conversion.
class MyNumber {
private:
int value;
public:
MyNumber(int v) : value(v) {} // Constructor accepting int for conversion
void display() {
cout << "Value: " << value << endl;
}
};

int main() {
int num = 10;
/*
This calls constructor of MyNumber.
The call would be of this kind `MyNumber obj(num);`. I'm not sure if
this actually happens but it can be a good trick to remember this by.
*/
MyNumber obj = num; // Primitive to class type conversion.

obj.display();
return 0;
}

Class type to primitive type (Class to Primitive)

  • This involves converting an object of a class type to a primitive data type.
  • You can define conversion operators in your class to facilitate this conversion.
class MyNumber {
private:
int value;
public:
MyNumber(int v) : value(v) { }
operator int() const { // Conversion operator to int
return value;
}
};

int main() {
MyNumber obj(20);
/*
Here, I want to convert the type from 'MyNumber' to 'int', that's why
I've used `operator int()`. If I wanted to convert to 'float' or 'double',
I'd have used `operator float()` and `operator double()` respectively.
*/
int num = obj; // Class to primitive type conversion
cout << "Value: " << num << endl;
return 0;
}

One class type to another (Class to Class)

  • This involves converting an object of one class type to another class type.
  • You can define conversion constructors or conversion operators in your class to facilitate this conversion.
class DistanceInMeters {
private:
double meters;
public:
DistanceInMeters(double m) : meters(m) {}
operator double() const { // Conversion operator to double
return meters;
}
};

class DistanceInFeet {
private:
double feet;
public:
DistanceInFeet(double f) : feet(f) {}
DistanceInFeet(const DistanceInMeters& obj) { // Conversion constructor from DistanceInMeters
feet = obj; // Implicit conversion using conversion operator (Class to Primitive)
feet *= 3.28084; // Conversion factor from meters to feet
}
void display() {
cout << "Value in Feet: " << feet << endl;
}
};
int main() {
DistanceInMeters objMeters(10);
// Don't confuse this with copy assignment because for copy assignment to work
// both sides of the assignment operator should have same class type, but here
// they're different.
DistanceInFeet objFeet = objMeters; // Class to class conversion
objFeet.display();
return 0;
}

A brief explanation of what’s happening in the main function:

  • An object objMeters of type DistanceInMeters is created with a value of 10 meters.
  • An object objFeet of type DistanceInFeet is initialized with objMeters. This triggers the conversion constructor of DistanceInFeet, which accepts an object of DistanceInMeters class as reference.
  • Inside the conversion constructor, feet = obj; invokes the conversion operator of DistanceInMeters class, which returns the value in meters. Then, it multiplies the value by 3.28084 to convert meters to feet.
  • Finally, the display function of DistanceInFeet is called to print the converted value in feet.

Primitive to Primitive

  • These conversions are automatically done by the compiler.
  • For ex: int to float , float to int , int to bool etc.
  • A point to remember here is that, it could lead to data loss, especially when converting between floating-point and integer types.

--

--

Lokesh Bihani

Software Engineer passionate about System Design, DevOps, and ML. I try to simplify complex tech concepts to help others learn while deepening my own knowledge.