Сохранить права доступа при использовании оператора '->'

У меня два класса,

      template<class Type>
class SafePtr {
public:
    SafePtr() {}
    ~SafePtr() {}

    void Lock(Type* data, void* key)
    {
        if (!pKey)
        {
            pKey = key;
            pData = data;
        }
    }

    Type* Unlock(void* key) const
    {
        if (key == pKey)
            return pData;
    }

    Type* operator->() 
    {
        return pData;
    }

private:
    Type* pData = nullptr;
    void* pKey = nullptr;
};

template<class Type>
class SafePtrArray {
public:
    SafePtrArray() {}
    ~SafePtrArray() {}

    template<class... Args>
    SafePtr<Type> CreatePtr(Args&&... args)
    {
        Type* data = new Type(args...);
        ptrs.insert(ptrs.end(), data);

        SafePtr<Type> ptr;
        ptr.Lock(data, this);
        return ptr;
    }

    Type* UnlockPtr(const SafePtr<int>& ptr)
    {
        return ptr.Unlock(this);
    }

    void Destroy(const SafePtr<int>& ptr)
    {
        Type* pointer = ptr.Unlock(this);

        for (auto itr = ptrs.begin(); itr != ptrs.end(); itr++)
        {
            if ((*itr) == pointer)
            {
                delete pointer;
                ptrs.erase(itr);
            }
        }
    }

private:
    std::vector<Type*> ptrs;
};

Объяснение:
Цель состоит в том, чтобы защитить указатель, чтобы пользователь мог получить доступ к его элементам, но не мог манипулировать его фактическим указателем (в основном, преждевременно удалять его). А также мне нужно хранить все указатели в массиве, чтобы при уничтожении родительского объекта я мог автоматически уничтожить все выделенные указатели.
Для этого я использую два класса и . создает и сохраняет указатели, оборачивает их в и возвращает пользователю. является просто оболочкой и не должна позволять пользователю получать доступ к базовому указателю, но разрешать ему доступ к своим членам.

Сначала все работало нормально, но вскоре я обнаружил эту ошибку,

      int main()
{
    SafePtrArray<int> ptr;
    auto pInt = ptr.CreatePtr();
    int* i = pInt.operator->();     // Users can get access to the underlying pointer using this.

    ptr.Destroy(pInt);
}

Итак, мой вопрос: есть
ли способ запретить пользователям доступ к базовому типу и запретить им манипулировать указателем, имея привилегию доступа к его членам?

2 ответа

Я все еще думаю, что вы пытаетесь решить проблему, которая больше связана с возможными недостатками в дизайне API/кода, документации или с отсутствием знаний C++ у того, кто его использует, с «решением», которое имеет больше минусов, чем плюсов.

Если программист C++ не знает, что такое право собственности, или не уважает его, и вслепую удаляет объекты или освобождает память указателей, тогда проблем будет гораздо больше. Скорее всего, вы перенесете проблему просто в другую часть кода.

С учетом сказанного, самое близкое, что вы можете сделать, чтобы не показывать указатель прямо сейчас, это что-то вроде этого:

(Код — это всего лишь доказательство концепции, поэтому такие вещи, как callможет надо доработать)

      #include <iostream>
#include <string>

struct Test {
  void foo(int x, int y, std::string str) {
    std::cout << x << " " << y << " " << str << std::endl;
  }

  double test = 0.5;
};

template <typename T>
struct Ptr {

  template <auto M, typename... Args>
  auto call(Args... args) {
    return (obj.*M)(std::forward<Args>(args)...);
  }

  template <auto M>
  auto get() { 
    return (obj.*M);
  }

protected:
  T obj;
};

int main() {
  Ptr<Test> p;

  p.call<&Test::foo>(1, 2, "hello");
  std::cout << p.get<&Test::test>() << std::endl;

  return 0;
}

Но я все еще не думаю, что это хороший подход.

И пользователь все еще может возиться с кодом и сделать что-то плохое, например:

      int main() {
  Ptr<Test> p;
  
  delete &p;

  return 0;
}

Или это, что, безусловно, является неопределенным поведением, но на самом деле это не имеет значения, поскольку удаление не принадлежащего объекта также приведет к неопределенному поведению в какой-то момент:

      template<typename T>
struct Ptr {
protected:
    T *obj;
}

template<typename T>
struct Ptr2 {
public:
    T *obj;
};

int main()
{
    Ptr<Test> p;
    Ptr2<Test> *p2 = reinterpret_cast<Ptr2<Test>*>(&p);
    
    std::cout << p2->obj << std::endl;
}

Так что нет защиты опять же от таких вещей.

Помимо показанного кода, есть предложение для отражения , которое уже полностью завершено, что позволило бы получать информацию о членах типа, но это не было добавлено в c++20, и одно для метаклассов , которого также нет в стандарт еще.

С этими двумя предложениями вы могли бы реализовать что-то более полезное. Но мои опасения по поводу преимуществ этого остаются.

Есть ли способ запретить пользователям доступ к базовому типу и запретить им манипулировать указателем, имея привилегию доступа к его членам?

При определенных условиях нет, это невозможно. Если в основе является стандартным классом компоновки, тогда предоставление доступа к первому нестатическому элементу данных, не являющемуся битовым полем, нарушает вашу цель. (Предостережение: предоставление доступа только к значению члена — это отдельная история.) Адрес этого члена можно преобразовать в указатель на базовый объект с помощьюreinterpret_cast, что позволяет вызывать на этом указателе. (Ну, «разрешает» в том смысле, что вызов является синтаксически корректным. Для «разрешает» не так уж много другого имеет значение, поскольку мы все равно движемся к неопределенному поведению.)

Для классов с нестандартной компоновкой, вероятно, существуют специфичные для компилятора (непереносимые) методы для достижения того же эффекта (преобразование адреса члена данных в указатель на базовый объект). У компилятора нет причин активно пытаться помешать таким вещам.

Если программист решил вызвать неопределенное поведение, вы мало что можете сделать, чтобы остановить его.

Другие вопросы по тегам