Search code examples
c++constantspimpl-idiom

why constant function of implement class not be accessed in PIMPL?


I want to have a try in PIMPL in C++.

In my case, I am using operator() to access private member.

The interface class A and implement class AImpl all have operator() const and operator().

The code shown as follows:

#include <iostream>
class AImpl
{
public:
    explicit AImpl()
    {
        x = 0;
    }
    const int &operator()() const
    {
        std::cout << "const access in AImpl" << std::endl;
        return x;
    }
    int &operator()()
    {
        std::cout << "not const access in AImpl" << std::endl;
        return x;
    }

private:
    int x;
};

class A
{
public:
    A()
    {
        impl = new AImpl;
    }
    ~A()
    {
        delete impl;
    }
    const int &operator()() const
    {
        std::cout << "const access in A" << std::endl;
        return impl->operator()();
    }
    int &operator()()
    {
        std::cout << "not const access in A" << std::endl;
        return impl->operator()();
    }

private:
    AImpl *impl;
};

int main()
{
    A non_const_a;
    std::cout << non_const_a() << std::endl;

    const A const_a;
    std::cout << const_a() << std::endl;
}

I compile the program with follow command

g++ Main.cpp

The result showns that:

# ./a.out
not const access in A
not const access in AImpl
0
const access in A
not const access in AImpl
0

It can be seen from the result that:

A's const member function const int &A::operator()() const call the int &AImpl::operator()(), but not call the const int &AImpl::operator()() const.

Why this happened?

In PIMPL case, I want the member function in A and and AImpl are one-to-one correspondence.

I want let const int &A::operator()() const call const int &AImpl::operator()() const.

How can I modify my code to achieve that? Does the modification will slow down any performance?

The mention above is a simple case. In real case, the A is a container and will widly used in my code, thus I do not want the modification slow down the performance.

I apologize if it is a stupid problem. Thanks for your time.


Solution

  • You are looking different const-ness behaviour.

    AImpl const* is not the same AImpl* const.

    When A is const, you get the impl ptr to be of type AImpl* const. When Ais not-const, you get the impl ptr to be of type AImpl*.

    In both cases, the data to which the pointer points is always AImpl* (non-const). The pointer itself is the one that may or may not be const, thus allowing you to change to where it points or not. But the data to which it points is always non-const.

    In order to solve this issue, you really need to get a pointer of either AImpl* or AImpl const* (or even better, AImpl const * const, denoting that both, the pointer and the data to which it points are constant)1. You have several ways to do this:

    You can just add some accessors to the pointer:

    AImpl* getImpl() { return impl.get(); }
    AImpl const *getImpl() const { return impl.get(); }
    

    This approach has the inconvenient that you have to remember to always use the accessors to get the correct const version, while using directly the impl pointer may get you incorrect behaviour.

    Another approach would be to add a container template class, which holds the pointer and declares different operator() accessors, returning the correct type for each access type. A basic example of this would be:

    template <typename _Tp>
    class pimpl_ptr
    {
    public:
        pimpl_ptr(_Tp *q, U&&... u): fPtr(q) { }
    
        _Tp const* operator->() const noexcept
        {
            return fPtr;
        }
        _Tp* operator->() noexcept
        {
            return fPtr;
        }
    private:
        _Tp *fPtr;
    };
    

    with the added benefit that this class could also implement RAII, managing the destruction of the pointer itself.


    1 This is not really needed.
    Since the class returns the pointer through a function, the pointer itself is returned by value, meaning that the caller gets a copy of the pointer. So any change to this pointer will affect only the returned copy, not the one inside the class.
    Declaring the pointer also const would be necessary if the functions would return the pointer by reference (AImpl*&), which is almost never the case. In this case, the const version would have to actually return AImpl const* const&, in order to prevent modifications to both, the pointer inside the class and the pointed data.