Páginas

segunda-feira, 19 de fevereiro de 2018

Controlling public methods access without using friends

Programming is a hard task that can become overwhelming as systems grows and more dependencies and different libraries, and internal and external code are used together to execute the many tasks needed by the resulting software. In order to reduce this complexity, there are many coding guidelines to help us – which one of them is “make your API easy to use correctly, hard to use incorrectly”. Careful planning and functionalities “blocking” can make a project to proceed smoothly, where it would be a nuclear insanity reactor otherwise.

There are many techniques and idioms to try to make a class hard to misuse in C++ (and many other languages), but the focus of this post is on a specific concept: controlling who can access the private methods of which classes. I’ll talk about one way of achieving this.

This private access control can be really useful. Let’s say, for example, that you have a class that can be publicly used. However, for reasons like instances tracking or resources management, you want it to be instantiated only by a specific Factory and consumed only by a certain type. This can be achieved by making the constructor private, and then using friend methods or classes.

class Medicine
{
    friend class MedicineFactory;
    friend class SickPerson;
public:
    ~Medicine();
    std::string GetName();
    
private:
    explicit Medicine(std::string name);
    void Consume();
    
    std::string m_Name;
};

In the above example, we have the medicine class. It’s constructor and Consume methods are both private, so they’re accessible only to both friend classes: MedicineFactory and SickPerson. No other classes can instantiate a medicine or consume it. But there is a catch: since they’re friend classes, it is then possible that, by accident or misinformation, a MedicineFactory end up accidentally consuming a medicine or, even worse, a SickPerson to home brew medicine.

What we want to do here is to make sure that only MedicineFactory instantiates medicine, and only SickPerson consumes it. Of course, a solution that wouldn’t get in the way of helping methods and classes such as std::make_unique or std::make_shared is a plus. There are ways to handle this situation, but I’ll focus on the Passkey Idiom. This idiom allows us to make methods callable only by instances of classes that can instantiate a given key. Using classes as keys, the Medicine class could be written as:

class Medicine
{
public:
    Medicine(std::string name, const PasskeyIdiom<MedicineFactory>&);
    ~Medicine();
    
    std::string GetName();
    void Consume(const PasskeyIdiom<SickPerson>&);
    
private:
    std::string m_Name;
};

What changed is that now the constructor and Consume method both have a new parameter.As long as we can guarantee that only MedicineFactory can instantiate PasskeyIdiom, then only methods within the factory class will be able to create new instances of Medicine even with the public constructor. Of course, the same applies to the Consume method and SickPerson class. Note that the parameters are constant references to the PasskeyIdiom class. This allows us to bind the parameters to rvalues, shortening the coded needed to call these methods.

The trick lies within the PasskeyIdiom class template:

template <typename T>
class PasskeyIdiom
{
private:
    friend T;

    PasskeyIdiom() = default;
    PasskeyIdiom(const PasskeyIdiom&) = default;
    PasskeyIdiom(PasskeyIdiom&&) = default;

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

    ~PasskeyIdiom() = default;
};

The three constructors, the destructor and the assignment operator are all defaulted on private accessibility. The “friend T” constructor, possible from C++ 11 onwards, makes it so that only instances or static methods of T can instantiate or uses new objects from the idiom, specialized to T itself. It works quite well – even with smart pointers.

Here’s a full code testing the idiom:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
#include<iostream>
#include<memory>



class Medicine;
class MedicineFactory;
class SickPerson;



template <typename T>
class PasskeyIdiom
{
private:
    friend T;

    // Defaulted on private, to avoid public defaults
    PasskeyIdiom() = default;
    PasskeyIdiom(const PasskeyIdiom&) = default;
    PasskeyIdiom(PasskeyIdiom&&) = default;

    ~PasskeyIdiom() = default;

    PasskeyIdiom& operator=(const PasskeyIdiom&) = delete;
};



class Medicine
{
public:
    Medicine(std::string name, const PasskeyIdiom<MedicineFactory>&)
        : m_Name(name) {}

    void Consume(const PasskeyIdiom<SickPerson>&)
    {
        std::cout << "Medicine " + m_Name + " has been consumed" << std::endl;
    }

private:
    std::string m_Name;
};



class MedicineFactory
{
public:
    std::unique_ptr<Medicine> InstantiateMedicine(std::string name)
    {
        return std::make_unique<Medicine>(name, PasskeyIdiom<MedicineFactory>{});
    }
};



class SickPerson
{
public:
    void ConsumeMedicine(Medicine& medicine)
    {
        medicine.Consume(PasskeyIdiom<SickPerson>{});
    }
};



int main() {
    MedicineFactory medicineFactory{};
    auto medicine = medicineFactory.InstantiateMedicine("Antibiotic");
    
    SickPerson sickPerson{};
    sickPerson.ConsumeMedicine(*medicine);

    // These won't compile

    /*

    // Private constructors and destructors
    PasskeyIdiom<MedicineFactory> medicineFactoryPasskey{};


    class IllegalMedicineFactory
    {
        // Cant use another class as passkey
        std::unique_ptr<Medicine> InstantiateMedicine(std::string name)
        {
            return std::make_unique<Medicine>(name, PasskeyIdiom<MedicineFactory>{});
        }

        // No overloaded constructor on Medicine
        std::unique_ptr<Medicine> InstantiateMedicine(std::string name)
        {
            return std::make_unique<Medicine>(name, PasskeyIdiom<IllegalMedicineFactory>{});
        }
    }

    */

    return 0;
}
All code in this page has been colored by hilite.me.

Nenhum comentário:

Postar um comentário