【问题标题】:Preventing the use of an overridden non-virtual function - The correct way?防止使用被覆盖的非虚拟函数 - 正确的方法?
【发布时间】:2020-03-19 13:45:52
【问题描述】:

所以这是我的第一个问题。我已经搜索了该网站,找到了一些东西并应用了其中给出的建议,但我仍然不确定我是否以正确的方式做到了。

我正在开发一个模板库,这是我对 BST 类模板的实现:

template <class T>
class bstree
{
private:
    struct bstnode
    {
        bstnode* pRight;  //node to the right (greater)
        bstnode* pLeft;   //node to the left (lesser)
        bstnode* pParent; //parent node
        T        mValue;  //contents
    };

    class bstnodeiterator : public _iterator_base<T, bstree<T>>
    {
    public:
        bstnodeiterator(bstnode* pNode = nullptr, bstree<T> pCont = nullptr)
                  : _mpNodePtr(pNode), _mpCont(pCont) {}

        //functions from _iterator_base<>
        bool             is_null() const       { return (_mpNodePtr == nullptr); }
        const bstree<T>* get_container() const { return this->_mpCont; }
        //get_pointer() is intentionally not defined.

        //operators (e.g. increment, decrement, advance by, dereference, etc)
        //go here!
        //...

    private:
        friend class bstree<T>;

        //member elements:
        bstree<T>* _mpCont;    //the container that the iterator is created by
        bstnode*   _mpNodePtr; //the actual pointer pointing to the bst-node of '_mpCont'
    };

public:
    using val      = T;
    using val_ref  = T&;
    using val_ptr  = T*;
    using iter     = bstnodeiterator;

public:
    iter begin() const;
    iter end() const;

    //other public member functions (e.g. insert(), remove(), etc.) go here!
    //...

private:
    bstnode* _mpRoot;   //The root node of the BST
    size_t   _mSize;    //The number of elements in the container (guaranteed O(1))
};

bstnodeiterator::get_container()bstnodeiterator::is_null() 派生自 iterator_base&lt;&gt;,它是所有其他容器(例如 vector&lt;&gt;fixed_list&lt;&gt;map&lt;&gt; 等)的迭代器的基类:

template <class T, class Cont>
struct _iterator_base
{
    virtual           bool  is_null() const = 0;
    virtual     const Cont* get_container() const = 0;
    /*virtual*/ const T*    get_pointer() const /* = 0*/;
};
//is_null() and get_container() should be defined in derived classes
//because they are used everywhere in the library!
  • 需要定义以上所有三个函数,因为它们在整个库中的其他任何地方都使用(例如,在算法中,iterator_helper 类等)。

由于 BST 是已排序元素的容器,因此不应动态更改节点的内容。因为这会破坏树的排序结构。 因此,我想阻止程序员使用get_pointer() 即使它返回一个指向内容的 const 指针,它仍然可以通过T 的成员函数进行更改(例如,如果@987654335 @ 是 std::string 然后可以通过 std::string::assign() 更改内容),我不想要这个。

所以,我在基类中将函数 _iterator_base&lt;*,*&gt;::get_pointer() 设为非虚拟。而且它没有在派生类bstnodeiterator 中定义。所以,如果程序员从派生类中调用它...

bstree<std::string> strTree = { "a string", "another string", "yet another string", "test string" };
//inserted some other elements
bstree<std::string>::iterator it = strTree.begin();
//*it = "something else"; --> this won't work, because read-only dereferencing is allowed in the class.
it.get_pointer()->assign("something else"); //this will break the tree.

...那么编译器会给出链接错误:unresolved external symbol " ... ::get_pointer()".

这是正确的方法吗?你怎么看?

编辑:

我刚刚尝试取消引用和修改:

bstree<std::string> strTree = 
{ 
   "a string", 
   "another string", 
   "yet another string", 
   "test string" 
};

bstree<std::string>::iter it = strTree.begin();
(*it).assign("modified string"); // ----> error!
std::string pB0 = strTree.begin(); // ----> error

const std::string pB = strTree.begin();
pB->assign("modified string"); // ----> error!

...它没有编译。但是,如果我将最后一行更改为:

it.get_pointer()->assign("modified string");

...它可以无错误地编译、运行和工作!

编辑 2:

我终于找到了问题的根源:typedefs。

我没有在原始问题中显示typedefs,以使其看起来更简单、更易于阅读。在原始代码中,bstree&lt;&gt; 范围内有一个using val_ptr = T*;,而我在bstnodeiterator 范围内使用这个typedef

template <class T>
class bstree
{
public:
    using val = T;
    using val_ref = T&;
    using val_ptr = T*;

private:
    class bstnodeiterator : public _iterator_base<T, bstree<T>>
    {
        //c'tor comes here!

        const val_ptr get_pointer() { return (_mPtr ? &_mPtr->_mVal : nullptr); }
        //...
    };

 //...
 };

如果我定义了上面给出的函数,那么我可以从get_pointer() 的返回指针调用std::string::assign()。但是,如果我将函数的返回类型更改为const val*,则无法调用string::assign()

我终于意识到这两种类型是不同的。可能编译器会将const 放在其他地方。

【问题讨论】:

  • Even if it returns a const pointer to the contents it can still be changed via member functions of T 如果返回指向 const 的指针,则无法更改对象。
  • 我不确定我是否理解正确,但是const std::string 的内容不能更改,这就是const 关键字的全部意义所在。您不能在 const 对象上调用任何非 const 方法。
  • std::string::assign 没有任何 const 限定的覆盖。您不能在任何 const 字符串上调用它。 Demo
  • @KaenbyouRin 我刚刚尝试使用 bstree::begin() 访问树的第一个节点,然后调用 bstnodeiterator::get_pointer() 返回 const ptr 然后调用 std::string::分配()。 rStdStrTree.begin().get_pointer()-&gt;assign("newly assigned string"); 并且内容变了。我做错了什么?
  • @tkausl 我知道,但我认为我做错了什么,因为正如我在之前的评论中所说,我可以更改节点的内容。

标签: c++ inheritance polymorphism virtual-functions pure-function


【解决方案1】:

响应 OP 的第二次编辑:

别名不像宏。

如果你写using PtrType = T*,那么const PtrType实际上是 相当于T* const,它是一个指向T 对象的常量指针,而不是一个指向常量T 对象的指针。使用别名时,总是在顶层添加更多的 cv 限定符。这很直观——如果PtrType 是指向T 的指针,那么const PtrType 应该是指向T 的常量指针。


根据问题,如果你不想让用户调用虚函数,就设为protected,这样派生类可以实现它,但外部用户不能调用它。


您的返回类型很可能是bstnodeiterator::get_pointer() T*(而不是const T*)。

您可能正在经历 c++ covariant return types 的陷阱。

  • 这两种类型都是类的指针或引用(左值或右值)。不允许使用多级指针或引用。

  • Base::f() 的返回类型中的引用/指向类必须是(或相同)的明确且可访问的直接或间接基类 Derived::f() 的返回类型的引用/指向类。

  • Derived::f() 的返回类型必须等于或小于 Base::f() 的返回类型的 cv 限定。

注意:c++引用没有“(or is the same as)”子句,但为了与标准保持一致而添加了“”

因此,如果函数覆盖返回类型为 const std::string* 的函数,则 std::string* 是有效的返回类型。

考虑这个例子:

#include <string>

std::string s = "Hello, world";

struct Base {
    virtual const std::string* foo() = 0;
};

struct Derived : Base {
    std::string* foo() override {
        return &s;
    }
};

int main() {
    Derived d;
    d.foo()->assign("You can do this.");
    return 0;
}

上面的代码compiles:可以修改d.foo()指向的字符串,因为它返回一个std::string*

【讨论】:

  • 我确信get_pointer() 返回const T*。你能看看问题中的编辑部分吗?
  • 能否请您看一下我的第二次编辑。我知道它会变得很糟糕,但请......我认为问题的根源是不同的。
猜你喜欢
  • 2012-06-19
  • 2014-01-05
  • 2018-01-28
  • 1970-01-01
  • 1970-01-01
  • 2021-11-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多