0

Let's say we have a library that has a class "Car" which has some private members of class "Wheel".

We want the user to be able to create a "Car" and use the "Drive" function(ality) of it. This will also turn the wheels.

However, we want to avoid the user to create a "Wheel" by itself and turn it w/o the use of a car. To avoid them using the library improperly.

See example code below, where the main.cpp is the user code and the rest is the library.


main.cpp

#include <iostream>
#include "car.h"

int main()
{
    std::cout << "Hello World" << std::endl;
    Car myCar = Car();
    myCar.drive();
    
    //This should not be possible!
    Wheel someWheel = Wheel();
    someWheel.rotate();
    return 0;
}

car.h

class Wheel;//Forward declaration (to avoid '#include "wheel.h"', which would make it accessible in main.cpp.
class Car {
public:
    Car();
    void drive();
private:
    Wheel leftWheel;//Error, requires a pointer
};

car.cpp

#include <iostream>
#include "car.h"
#include "wheel.h"

Car::Car(){
    leftWheel = new Wheel();
};
void Car::drive(){
    std::cout << "Vroom!"  << std::endl;
    leftWheel.rotate();
}

wheel.h

class Wheel {
public:
    Wheel();
    void rotate();
};

wheel.cpp

#include <iostream>
#include "wheel.h"

Wheel::Wheel(){
};
void Wheel::rotate(){
    std::cout << "Wheel: *Rotates*"  << std::endl;    
}

We've found that by including the header file in the Car.cpp, we can achieve this. However, when we make a forward declaration of 'Wheel', we can only use a pointer to Wheel as a private member from Car.

Otherwise it leads to:

car.h:12:11: error: field ‘leftWheel’ has incomplete type ‘Wheel’

So my questions are:

  • Should we at all hide 'Wheel' from the user, for his own 'safety'? Or is there a stronger reason not to do it?
    • If so, can we achieve this without the use of a pointer
      • If not, do we have to explicitly make a deconstructor that deletes the instance from memory?
Paul
  • 675
  • 1
  • 5
  • 26
  • Things that come to mind are : Let car depend on abstract base class (interface) of wheel only (in its declaration), you can include in the car's definition (source file). Make the members of Car, std::unique_ptr leftwheel; etc.. Or you can change the Car class to use the [pimpl pattern](https://en.cppreference.com/w/cpp/language/pimpl), which will also hide ALL its implementation from other classes at compile (and runtime) time. It is also a good pattern to improve build speeds (parallel building) – Pepijn Kramer Jun 08 '23 at 09:55
  • Some libraries put the components in a nested namespace, like `detail` or `Private`, and then tell the users not to access that part. Is that not enough protection? – BoP Jun 08 '23 at 09:58
  • 2
    Why would you care if the users build their own cars or motorcycles or tractors or airplanes using the wheels provided by the library? What's wrong with that? – n. m. could be an AI Jun 08 '23 at 10:00
  • 1
    Hi, If ```Wheel``` are not for the user, technically you can achieve it as you did with a pointer (pimpl idiom as mentioned by PepijnKramer). You can also declare all its methods private and let ```Car``` be a friend (it should be better to use some trait (in order to let different classes use ```Wheel```) and [this attorney pattern](https://stackoverflow.com/questions/3217390/clean-c-granular-friend-equivalent-answer-attorney-client-idiom)). Besides encapsulating ```Wheel``` in some dedicated n,amespace, properly documented as *not for client* can do no harm (see Bop comment). – Oersted Jun 08 '23 at 10:03

1 Answers1

2

Depending on the access you want the user to give to the Wheel class, simply restricting some members of the class to non-public visibility and making Car a friend can do the trick.

Alternatively you could make Wheel a protected nested class of a supertype.

Both of these alternatives result in a compiler complaining about the inaccessibility of members/types when trying to create a Wheel without a Car. This is closer to the message you're trying to convey ("Keep your hands of my Wheel class.") than a message about the type of Wheel being incomplete. (The latter is likely to be interpreted as "You're missing the correct #include".) Furthermore it avoids an extra dynamic allocation.

namespace UseFriends
{
class Car;

class Wheel
{
    friend class Car;

    Wheel() = default;
        
    Wheel(Wheel&&) = default;
    Wheel& operator=(Wheel&&) = default;

    Wheel(Wheel const&) = default;
    Wheel& operator=(Wheel const&) = default;
public:
    void rotate()
    {
        std::cout << "Wheel: *Rotates*" << std::endl;
    }
};

class Car
{
public:
    Car()
    {
    }

    void drive()
    {
        std::cout << "Vroom!" << std::endl;
        leftWheel.rotate();
    }
private:
    Wheel leftWheel;
};

} // namespace UseFriends

namespace UseNestedClass
{
class Vehicle
{
protected:
    class Wheel
    {
    public:
        void rotate()
        {
            std::cout << "Wheel: *Rotates*" << std::endl;
        }
    };
};

class Car : private Vehicle
{
public:
    Car()
    {
    }

    void drive()
    {
        std::cout << "Vroom!" << std::endl;
        leftWheel.rotate();
    }
private:
    Vehicle::Wheel leftWheel;
};

} // namespace UseNestedClass

int main()
{
    {
        using Car = UseFriends::Car;
        using Wheel = UseFriends::Wheel;

        std::cout << "Hello World" << std::endl;
        Car myCar = Car();
        myCar.drive();

        Wheel someWheel = Wheel(); // compiler error: UseFriends::Wheel::Wheel() is inaccessible
        someWheel.rotate();
    }

    {
        using Car = UseNestedClass::Car;
        using Wheel = UseNestedClass::Vehicle::Wheel; // compiler error: UseNestedClass::Vehicle::Wheel is inaccessible

        std::cout << "Hello World" << std::endl;
        Car myCar = Car();
        myCar.drive();
    }
}
fabian
  • 80,457
  • 12
  • 86
  • 114
  • Interesting, we did learn about 'Friend class', but didn't expect it to be applicable for this situation. Though it feels more 'safe' as using pointers. – Paul Jun 08 '23 at 13:10