Modern (Effective) C++ - Pimpl Idiom from C++11 onwards

This article is a continuation of Pimpl idiom prior to C++11.

So as mentioned earlier, we should NOT use raw pointers from C++11 onwards unless until it is absolutely unavoidable. Let's go ahead and replace the raw pointer in Car.h by a std::unique_ptr

// Car.h
#pragma once
#include <memory>
#include <string>
class Car
{
public:
Car();
void move(int src, int dest) const;
void addFeature(std::string, std::string);
private:
struct CarImpl;
std::unique_ptr<CarImpl> impl;
};
// Car.cpp
#include "Car.h"
#include <iostream>
#include <map>
struct Car::CarImpl
{
int numOfWheels;
std::string makeOfCar;
std::map<std::string, std::string> features;
};
Car::Car() : impl(std::make_unique<Car::CarImpl>())
{
}
void Car::move(int src, int dest) const
{
std::cout << "Moved from " << src << " to " << dest << '\n';
}
void Car::addFeature(std::string key, std::string val)
{
this->impl->features[key] = val;
}

The code above compiles fine. But in our main file, if we do something as simple as this, the compiler throws various compiler errors.

#include "Car.h"
int main()
{
Car c;
}
view raw mainFIle.cpp hosted with ❤ by GitHub

The errors are -
1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.12.25827\include\memory(2125): error C2027: use of undefined type 'Car::CarImpl'
1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.12.25827\include\memory(2125): error C2338: can't delete an incomplete type
view raw errors.cpp hosted with ❤ by GitHub
This is because the Car::CarImpl is incomplete at the point where compiler generated functions are usually created i..e Car.h file.
Hence we have to move the auto generated functions to Car.cpp file, where Car::CarImpl is a complete type.

This includes the destructor and move functions. We define move instructions since objects with Pimpl idioms usually support move operations. The compiler generated versions of these functions should usually suffice.  But in order to generate it at the right place ( where the Car::CarImpl is a complete type), we have to do the following -


//Car.h
#pragma once
#include <memory>
#include <string>
class Car
{
public:
Car();
void move(int src, int dest) const;
void addFeature(std::string, std::string);
// Move functions declared
Car& operator=(Car&&);
Car(Car&&);
// Destructor declared
~Car();
private:
struct CarImpl;
std::unique_ptr<CarImpl> impl;
};
// Car.cpp
#include "Car.h"
#include <iostream>
#include <map>
struct Car::CarImpl
{
int numOfWheels;
std::string makeOfCar;
std::map<std::string, std::string> features;
};
Car::Car() : impl(std::make_unique<Car::CarImpl>())
{
}
void Car::move(int src, int dest) const
{
std::cout << "Moved from " << src << " to " << dest << '\n';
}
void Car::addFeature(std::string key, std::string val)
{
this->impl->features[key] = val;
}
// Move operation defined here
Car& Car::operator=(Car&& anotherCar) = default;
Car::Car(Car&& anotherCar) = default;
// Destructor defined here
Car::~Car() = default;
// mainFile.cpp
#include "Car.h"
int main()
{
Car c;
}
The above program compiles fine. But since we have defined move functions explicitly, we have to define copy instructions as well. Adding the copy operations, we get -

// Car.h
#include <memory>
#include <string>
class Car
{
public:
Car();
void move(int src, int dest) const;
void addFeature(std::string, std::string);
// Move functions declared
Car& operator=(Car&&);
Car(Car&&);
// Copy functions declared
Car& operator=(const Car&);
Car(const Car&);
// Destructor declared
~Car();
private:
struct CarImpl;
std::unique_ptr<CarImpl> impl;
};
// Car.cpp
#include "Car.h"
#include <iostream>
#include <map>
struct Car::CarImpl
{
int numOfWheels;
std::string makeOfCar;
std::map<std::string, std::string> features;
};
// constructor
Car::Car() : impl(std::make_unique<Car::CarImpl>())
{
}
void Car::move(int src, int dest) const
{
std::cout << "Moved from " << src << " to " << dest << '\n';
}
void Car::addFeature(std::string key, std::string val)
{
this->impl->features[key] = val;
}
// Move functions defined here
Car& Car::operator=(Car&& anotherCar) = default;
Car::Car(Car&& anotherCar) = default;
// Copy functions defined here
Car& Car::operator=(const Car& anotherCar)
{
*impl = *anotherCar.impl;
return *this;
}
Car::Car(const Car& anotherCar) : impl(std::make_unique<Car::CarImpl>(*anotherCar.impl))
{}
// Destructor defined here
Car::~Car() = default;
Most Pimpl idioms implementations use std::unique_ptr. But if we were to use std::shared_ptr, the compiler would have generated all the inbuilt functions without any errors. This is because deleter is not part of the type in shared_ptr. Thus std::shared_ptr uses more runtime data structures, and resulting in slightly slower code.
To conclude this article explains the correct way of implementing Pimpl idiom in Modern C++.


Modern (Effective) C++ - Pimpl Idiom from C++11 onwards Modern (Effective) C++ - Pimpl Idiom from C++11 onwards Reviewed by zeroingTheDot on February 08, 2018 Rating: 5

1 comment:

  1. Sands Casino NJ - Slots, Roulette, and Much More
    ‎Entertainment 바카라사이트 · ‎Casino Games · ‎About Us · ‎Dining 제왕 카지노 · ‎Entertainment · 샌즈카지노 ‎Lodging

    ReplyDelete

Powered by Blogger.