2

I come from Java that has a different way in handling what's private and has to be hided regarding a class implementation and it also has a garbage collector which means there is no need for a destructor.

I learned the basics of how to implement a class in c++ but I need to better understand how to implement a class, in particular the constructor and destructor, when using the pimpl idiom.

hpp file:

class MyClass{
public:
  MyClass();
  MyClass(std::vector<int>& arr);
  ~MyClass();

 
private:
  struct Impl;
  Impl* pimpl;
};

cpp file:

#include "MyClass.hpp"
using namespace std;


struct MyClass::Impl{
  vector<int> arr;
  int var;
};

I wrote the sample code of a class I need to work on(which means I can't change the way the pimpl idiom is used) using the pimpl idiom and I'm looking for an answer to these questions:

  • How do I implement the constructor to create an empty instance of MyClass?
MyClass::MyClass(){}
  • How do I implement the constructor to create an instance of MyClass with these arguments?
MyClass::MyClass(vector<int>& arr,int var){}
  • How do I implement the destructor?
MyClass::~MyClass(){}

EDIT:

How I would do it:

MyClass::MyClass(): pimpl(new Impl) {}

MyClass::MyClass(vector<int>& arr,int var): pimpl(new Impl) {
  pimpl->arr=arr;
  pimpl->var=var;
}

MyClass::~MyClass(){
  delete pimpl;
}

Spyromancer
  • 435
  • 1
  • 3
  • 10
  • 3
    The proper C++ term is not "deconstuctor", but a "destructor". And there is no cookie-cutter, paint-by-number recipe for implementing a destructor, any more than there is for implementing a constructor. Additionally, as shown, the public class must either implemented a copy-constructor or an assignment operator, or delete them. Otherwise, this will eventually end in tears. – Sam Varshavchik Feb 13 '22 at 20:28
  • 2
    Your best move would be to use a smart pointer for your pimpl member. That way you don't need to worry abut the destructor yourself, it will simply happen. – SoronelHaetir Feb 13 '22 at 20:34
  • 1
    Re: "[Java] has a garbage collector which means there is no need for a [destructor]" -- there are resources other than memory, and garbage collection doesn't address them. Java classes can have a `finalize()` method, which gets called when an object is about to be collected. So, while it's not **called** a destructor, it is, in fact, responsible for cleaning up resources, i.e., it plays the same role in Java as a destructor in C++, but it does it badly. Running out of file handles? Call the garbage collector! – Pete Becker Feb 13 '22 at 20:35
  • @SamVarshavchik An example of implementations might be enough to help me understand. Not the one and only way, that as you said does not exist, but just an example of a way. – Spyromancer Feb 13 '22 at 20:59
  • 1
    `std::unique_ptr pimpl;` in your class declaration. `pimpl(std::make_unique())` in your constructor (in source file) and default empty destructor also in source file. Alternatively, `pimpl(new Impl)` would also works but might be a little less efficient. – Phil1970 Feb 13 '22 at 21:03
  • 1
    Aren't there examples, that you're looking for, in the C++ textbook or whatever other learning material you're using? What, specifically, in your textbook's tutorial, on this topic, is unclear? – Sam Varshavchik Feb 13 '22 at 22:52
  • @Phil1970 I can't change the way the class and the struct are declared, I'm only allowed to implement the constructors and deconstructor with that specific way the pimpl idiom is used. – Spyromancer Feb 14 '22 at 14:24
  • @Sam Varshavchik I have to work with a data structure where pimpl is implemented that way and the way it is implemented is different from the exemples I found. – Spyromancer Feb 14 '22 at 14:28
  • 1
    I didn't comment about random examples from random examples that can be "found" somewhere. I was specifically referring to learning material from a textbook. C++ is just too complicated, the most complicated general purpose programming language in use today. It is not realistic to expect to learn C++ from Google searches, or Stackoverflow. Neither one, unfortunately, is a viable replacement for a textbook. If you're having trouble understanding something in your textbook, feel free to cite a brief excerpt or ask for a clarification, here. But this can't be summarized in a 1-2 paragraph answer. – Sam Varshavchik Feb 14 '22 at 22:24
  • @SamVarshavchik I'm using the website GeeksforGeeks as learning material for c++, here it is explained that it is better to use a unique pointer and also to define an assignment operator and copy constructor for the pimpl idiom. The way the pimpl idiom is used in the class sample, which is what I need to work on, is different and that is why I asked those questions. GeeksforGeeks is not a textbook but I think the concepts are explained well, here is the reference: https://www.geeksforgeeks.org/pimpl-idiom-in-c-with-examples/ – Spyromancer Feb 14 '22 at 23:07
  • 2
    Unfortunately it is not practical to learn the fundamentals of C++ by asking one "how do I do " type of a question at a time, on Stackoverflow or anywhere else. C++ is too complicated. It is not possible to fully explain the fundamental differences between using a unique_ptr, and not using it, in one or two paragraph-long answer. "How do I do " is too broad of a question here. The only way to correctly figure out how to do pimpl without a unique_ptr is by analyzing the ***entire*** class, understanding how it works, and figuring out how to make it work with pimpl. – Sam Varshavchik Feb 15 '22 at 00:21
  • @Spyromancer What is the problem with trivial solution like MyClass::MyClass() : pimpl(new Impl) { – Phil1970 Feb 15 '22 at 01:54
  • @Phil1970 To me none, I edited the post with a solution, can you tell me if it is wrong or not? – Spyromancer Feb 15 '22 at 09:56

1 Answers1

7

This is an example of a correct way to implement PIMPL idiom in modern C++:

foo.hpp

#pragma once

#include <memory>

class Foo {
public:
  Foo();
  ~Foo();

  Foo(Foo const &) = delete;
  Foo &operator=(Foo const &) = delete;

  Foo(Foo &&) noexcept;
  Foo &operator=(Foo &&) noexcept;

  void bar();

private:
  class impl;
  std::unique_ptr<impl> pimpl_;
};

foo.cpp:

#include "foo.hpp"

#include <iostream>

class Foo::impl {
public:
  impl() = default;
  ~impl() = default;

  impl(impl const &) = default;
  impl &operator=(impl const &) = default;

  impl(impl &&) noexcept = default;
  impl &operator=(impl &&) noexcept = default;

  void bar() { std::cout << "bar" << std::endl; }
};

Foo::Foo() : pimpl_(new impl{}) {}
Foo::~Foo() = default;

Foo::Foo(Foo &&) noexcept = default;
Foo &Foo::operator=(Foo &&) noexcept = default;

void Foo::bar() { pimpl_->bar(); }

main.cpp:

#include "foo.hpp"

int main(int argc, char const *argv[]) {
  Foo foo;
  foo.bar();
  return 0;
}

There are a few words that are to be said:

  • use a smart pointer (unique or shared) to hold reference(s) to 'impl' object. It'll help you like in JAVA control number of references to an underlying object. In this example, as just Foo class is gone 'impl' is automatically destroyed, there is no need to manually watch the lifetime of the 'impl' object
  • 'impl' MUST be always completely declared and defined in *.cpp file (or bunch of files) or internal *.hpp and/or *.cpp files so that a user of your front class (i.e., in this example 'Foo') that aggregates 'impl' is impossible to see any changes and content of your 'impl' class, that is why PIMPL idiom exists: to persist the interface of the front class for a user and all the changes mostly shall be done in the hidden 'impl' class
  • The destructor of the front class MUST be always defined in *.cpp file since the compiler must know how to destroy the 'impl' that is not seen thoroughly from the *.hpp file where a front class is declared
  • The special functions with rvalue-references (move-ctor and move-assignment operator) MUST also be defined in *.cpp file due to the same reason as in the previous clause
dpronin
  • 275
  • 1
  • 6
  • 1
    I'd be careful with `std::unique_ptr` and forward declaration of pimpl, for example https://stackoverflow.com/questions/13414652/forward-declaration-with-unique-ptr. – Jepessen Feb 13 '22 at 21:26
  • 3
    @Jepessen, what is wrong? Dtor and rvalue special functions seem to have been defined in foo.cpp. Is it still dangerous? – dpronin Feb 13 '22 at 21:29
  • @dpronin thank you for your detailed answer, if I understood well the code sample of MyClass is not the optimal way to use the pimpl idiom, the problem is that I need to work with methods of a class where pimpl is used that way and I don't know how to answer the questions I posted. I understood that I need to use pimpl->arr[i] and pimpl->var to read and modify the values of the members of the instance of MyClass but I still don't know the correct way to implement the constructors and deconstructor with the way the pimpl idiom is used. – Spyromancer Feb 13 '22 at 21:40
  • Should be using make_unique instead of new. – Taekahn Feb 13 '22 at 23:52
  • @Taekahn, there doesn't seem to be a reason to use make_unique in this example, I see it redundant, don't you? – dpronin Feb 14 '22 at 07:27
  • There is zero reasons I’m aware of to use new over make_unique and several for the opposite. I’d say it’s simpler and easier to use make_unique in all places. It’s also easier to teach “use make_unique” then it is trying to explain places to use one over the other. – Taekahn Feb 14 '22 at 13:10
  • @Jepessen `std::unique_ptr` with forward declaration is the well-known and **recommended way** to do pimpl. Any recent compiler should be able to compile the header without the definition of `Impl` but require it for the defaulted destructor in CPP file. I hope you use at least C++ 11... – Phil1970 Feb 15 '22 at 02:12
  • @Taekahn, you didn't mention a single reason to use 'make_unique' over 'new' in this example, nor did you call what advantages of make unique. The rule 'you must do so' I do not accept. I also could say 'there are zero reasons I'm aware of to use 'make_unique' over 'new' except several cases, for example, in c++11/c++14 where double new is used in one line and ordering of evaluating of function arguments is not strictly determined". As of c++17, make_unique has become useless. Do you worship 'make_unique' and believe it has to be used every time you like to make a unique pointer from scratch? – dpronin Feb 15 '22 at 14:35
  • @dpronin I didn't say you MUST do so, i said should. Please learn the difference. You said "there are zero reasons I'm aware of to use 'make_unique' over 'new' except several cases" I hope you realize how stupid that sounds. You literally contradict yourself at the end of the sentence. And, had you actually read what i wrote instead of getting all butt hurt for some unknown reason and rushing to reply, you would have seen that i did, in fact, provide 2 reasons. " it’s simpler and easier to use make_unique in all places. It’s also easier to teach “use make_unique” – Taekahn Feb 15 '22 at 14:45
  • @Taekahn, I know the difference between 'should' and 'must', but you don't seem to know the meaning of 'except'. I see you learn 'use make_unique everywhere because it is good' from grandfather and will not step back from the 100-years-old dogma. No offense given and none taken – dpronin Feb 15 '22 at 15:07
  • @dpronin No, those were merely two reasons that i gave. I use make_unique because this line "As of c++17, make_unique has become useless" is incorrect. You are still at risk of a memory leak in certain cases using new. But I don't have to support your code, so i'm leaving the discussion here. – Taekahn Feb 15 '22 at 15:31