【问题标题】:Preserve access privileges when using '->' operator使用“->”运算符时保留访问权限
【发布时间】:2021-04-27 06:16:52
【问题描述】:

我有两个课,

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;
};

解释:
目标是保护指针,以便用户可以访问其成员但不能操作其实际指针(主要是过早地删除它)。而且我还需要将所有指针存储在一个数组中,这样当父对象销毁时,我可以自动销毁所有分配的指针。
为此,我使用了两个类,SafePtr 和 SafePtrArray。 SafePtrArray 创建并存储指针并将它们包装在 SafePtr 中并将其返回给用户。 SafePtr 只是一个包装器,不应让用户访问底层指针,但允许他们访问其成员。

一开始还可以,但很快我就发现了这个错误,

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);
}

所以我的问题是,
有没有办法阻止用户访问底层类型并阻止他们在有权访问其成员的同时操作指针?

【问题讨论】:

  • 为什么不直接使用老旧的shared_ptr 及其朋友,让您的用户拥有熟悉的界面?
  • 你不能以通用的方式做到这一点。你需要那个的原因是什么?对于 stdlib 和其他库中的任何类型,用户通常可以访问他们不能删除的对象(及其指针),因为他们没有这些对象的所有权? (对于std::shared_ptr,std::unique_ptr,您可以调用get(),对于std::vector(),您可以调用data(),对于std::list,您可以检索元素的指针,...)
  • @D-RAJ 好吧,用户仍然可以做SafePtrArray&lt;int&gt; ptr; delete &amp;ptr; - 这当然是未定义的行为 - 但是删除您不拥有的对象通常也会导致未定义的行为(由于双重释放或访问已删除的对象)。听起来您要么想确保不了解该语言的用户不会做错任何事,要么想绕过代码或文档中可能存在的设计缺陷。
  • 鉴于operator-&gt; 明确提供了对指针的访问,我认为您的解决方案是停止认为 任何 合理的用户会以这种方式获取指针,而指针显然不是他们的玩,并注销任何会这样做的用户作为白痴。即使您管理了一些可怕的 hack 来启用您想要的行为,它仍然是 C++;他们可以直接访问内存,如果他们愿意,他们可以做可怕的非标准事情来访问该指针; 你的工作是防止意外做错事,让有动力的用户不可能滥用你的 API。
  • 典型的 C++ 方法是记录指针的所有权,声明任何删除我的指针的尝试都将导致未定义的行为,并且只是让一个坚定的程序员自取其辱。 C++ 不是一种安全的语言。如果你采用这种思维方式,std::shared_ptr 似乎会做你想做的事——注意它缺少release() 方法,不像unique_ptr。

标签: c++ pointers access-protection


【解决方案1】:

我仍然认为您尝试解决的问题更多地与 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;
}

所以没有保护再这样的事情。

除了显示的代码之外,还有一个针对 reflection 的提案,该提案现在功能已完成,可以获取有关类型成员的信息,但这并未添加到 c++20 中,还有一个针对 @987654322 的提案@ 这也不是标准。

通过这两个建议,您也许可以实现一些更好用的东西。但我仍然担心这样做的好处。

【讨论】:

    【解决方案2】:

    有没有办法阻止用户访问底层类型并阻止他们在有权访问其成员的同时操作指针?

    在某些条件下,不,这是不可能的。如果底层Type 是standard layout class,那么提供对第一个非静态非位域数据成员的访问会破坏您的目标。 (警告:仅提供对成员的 值 的访问是另一回事。) 该成员的地址可以通过@987654322 转换为指向基础对象的指针@,它允许在该指针上调用 delete。 (好吧,“允许”是指调用在语法上是有效的。“允许”并不重要,因为无论如何我们都会陷入未定义的行为。)

    对于非标准布局的类,可能有特定于编译器(不可移植)的方法来实现相同的效果(将数据成员的地址转换为指向底层对象的指针)。编译器没有理由积极尝试阻止此类事情。

    如果程序员决定调用未定义的行为,您几乎无法阻止它。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-05-16
      • 1970-01-01
      • 1970-01-01
      • 2012-05-17
      • 2011-10-13
      • 2011-03-13
      • 1970-01-01
      相关资源
      最近更新 更多