要求派生类至少重写一个虚函数

4

想象一下这个简单的基类:

struct simple_http_service
{
  virtual reply http_get(…);
  virtual reply http_post(…);
  virtual reply http_delete(…);
  // etc.
};

我希望防止用户继承此类而不覆盖至少其中一个方法,并防止他们实例化simple_http_service
有没有一些好的方式来做到这一点?

我大约有90%的把握,认为没有一种真正好的方法来做到这一点,特别是很难在C++中表达像“做这些中的一个”这样的约束条件。 - Aurojit Panda
3
嗯,看起来设计有些不稳定。如果他们可以忽略掉三个功能中的两个,那再多一个岂不是无所谓了? - GManNickG
3
如果您提供了所有这些函数的良好默认实现,那么如果有人想派生一个使用它们的类,这对您有什么影响呢? - CB Bailey
2
仍然听起来像是糟糕的设计。让我问一下:如果忽略了另外两个函数,它们的实现是什么?使用该类的用户如何知道哪个函数实际上被覆盖了? - GManNickG
@GMan 如果服务忽略了它们,它们将返回状态码501,表示“未实现”。 - Karl von Moor
1
我可以想到一个非常好的理由来实例化这个类,或者派生类不覆盖任何函数:测试所有函数在没有被覆盖的情况下是否正确运行。为什么要让这变得更加困难呢? - Mike Seymour
5个回答

5
这听起来像是一个非常奇怪的限制。一定要保护用户免受不正确使用的影响,但不要试图禁止那些你认为“没有意义”的事情。如果从您的类派生而没有覆盖其中任何一个函数是没有意义的,那么让用户覆盖尽可能多或尽可能少的函数,并相信他不会做出没有意义的派生而不覆盖任何函数的事情。用户这样做并没有什么害处,只是没有太大用处。
但是,如果您确实需要强制执行此限制(再次建议您重新考虑),则不要使用虚函数。相反,传递函数指针或函数对象(或std::function/boost::function)回调。使基类看起来像这样:
struct simple_http_service
{
  typedef std::function<reply (...)> func_type;
  reply http_get(...) { return get_func(...); }
  reply http_post(...) { return post_func(...); }
  reply http_delete(...) { return delete_func(...); }
  // etc.

private:
  func_type get_func;
  func_type post_func;
  func_type delete_func;
};

现在只需添加必要的构造函数(或自由/静态函数,以便您可以为其命名以避免歧义),以便当至少提供一个函数对象时才能实例化该类。

4
这是一个非常好的方法,很多人可能第一次使用时无法理解。 我使用它并推荐它。 对于普通读者:这意味着您不需要实现不同的服务,而是一个单一的服务,在构建期间可以将其设置为分派到操作处理程序。 您可以使用std::bind提供不同的服务实例,以控制在底层模型中分派到不同操作,或者您可以从单个服务中分派到不同的类,这种方法比继承更加灵活。 - David Rodríguez - dribeas

1

我认为所有这些函数都应该是纯虚函数。你发布的结构实际上是一个接口。如果不是所有函数都是必需的,那么派生结构体应该仅为与它们无关的函数提供空实现。


1
如果您只想强制基类为抽象类,请给它一个纯虚析构函数,并将其他函数设为普通虚函数。

0

我不太明白为什么你想要为其他两个函数提供默认实现,但在HTTP请求的情况下至少需要一个用户定义的函数。

如果所有函数都使用彼此来使用现有代码实现某些功能,则很清楚。

想象一下这个类的例子:

class Cls
{
  public:
    virtual std::string toString()=0;
    virtual std::string serialize()=0;
};

有一个类是可转换为字符串和可序列化为字符串的。但如果其中一个没有被实现,你想调用第二个,那么这将是一个选项:

class Cls
{
  public:
    virtual std::string toString() //calls serialize() by default
    {
      return this->serialize();
    }
    virtual std::string serialize() //calls toString()
    {
      return this->toString();
    }
    virtual ~Cls()=0; //force the class to be abstract
};  Cls::~Cls(){}

但现在有一个问题是从Cls派生,但没有覆盖至少一个函数。 如果没有覆盖,则在运行时会进入无限递归。 如果这是您的问题之一,则有一个运行时解决方案,下面的代码如果出现此类问题则不执行任何操作。

class Cls
{
  public:
    virtual std::string toString()
    {
      if ((void*)(this->*(&Cls::serialize)) != (void*)(&Cls::serialize))
      {//checks if the current implemetation is not equal to the default one
        return this->serialize();
      }
      else
      {
        return ""; //default return value
      }
    }

    virtual std::string serialize()
    {
      if ((void*)(this->*(&Cls::toString))!=(void*)((&Cls::toString)))
      {
        return this->toString();
      }
      else
      {
        return "";
      }
    }
    virtual ~Cls()=0;
};  Cls::~Cls(){}

这在GCC上编译通过,但会在屏幕上填充有关从funcptr到void *的奇怪转换的警告。至少它按预期工作。可能有一些元编程编译时解决方案,需要考虑一下。

附录1,测试成员函数之间的比较:

真的很奇怪。

#include <iostream>

class Base
{
    public:
        virtual int test()
        {
            //default imp
            return 0;
        }
};

class Der : public Base
{
    public:
        int test() override
        {
            //custom imp
            return 1;
        }
};

int main()
{
    Der a;
    Base b;
    std::cout << ((&Der::test) == (&Base::test)) << std::endl;//1: wrong
    //they are not equal
    //but for some reason the output is "true"
    //so direct comparisons do not work
    //however
    //if you convert that pointer to void*
    //everything works
    std::cout << ((void*)(&Der::test) == (void*)(&Base::test) ) << std::endl;      //0:right
    std::cout << ((void*)(a.*(&Base::test)) == (void*)(&Base::test) ) << std::endl;//0:right
    std::cout << ((void*)(b.*(&Base::test)) == (void*)(&Base::test) ) << std::endl;//1:right
    std::cout << ((void*)(&(a.test)) == (void*)(&(b.test)) ) << std::endl;         //0:right
    //so if you want to compare two functions
    //cast them to void*
    //works in any cases
    //'-Wno-pmf-conversions' compiler flag to inhibit warnings about casting
    system("pause");
    return 0;
}

附录2,获取函数真实地址的步骤:

Cls::serialize; //the function object itself
&Cls::serialize; //its member pointer
(void*)(&Cls::serialize); //extracting real address of the function for the comparison
(this->*&Cls::serialize); //again, a member pointer
(void*)(this->*&Cls::serialize); //extracting real address
//  │        │  └── Getting "pointer" to a member function of the class
//  │        └───── Then binding 'this' to that function, recall that if the function is virtual, '->*' returns a mamber pointer to it's custom implementation, not the default one.
//  └────────────── Then getting the real address


// it looks like 'this->*&Cls::serialize' does the same as '&this->serialize'
// but in practice it's not quite right
// '&this->serialize' returns the function pointer based on 'this' type
// therefore, comparison breaks, as inside of a base class 'this' always has the base type
// so you always receive the default implementation pointer
// 'this->*&Cls::serialize' does the same
// but now if 'serialize' is virtual
// it takes it into account and sends back its' custom implementation pointer

// (void*) casting is required because you need to compare functions' real addresses
// if you compare member pointers of a single virtual function
// they seem to be equal while they are, in fact, not

如何检查派生类是否实现了基类的虚函数在这里


一个成员指针不同于一个普通指针,通常它们之间不能相互转换。那个转换的含义是什么? - curiousguy
(void*)(this->*(&Cls::serialize))是什么? - curiousguy
与&(this->serialize)相同。我上传了一个带有测试和更多信息的答案编辑。 - drBright
编辑:(void*)(&this->serialize),忘记了强制转换。需要进行强制转换以获取成员函数的实际地址,而不是成员函数指针。 - drBright
但是用的是什么编程语言?GCC++吗? - curiousguy
Correct.------- - drBright

0

如果您知道要让派生类覆盖哪些方法,只需声明该方法为纯虚函数

例如,将http_get声明为纯虚函数:

struct simple_http_service
{
  virtual reply http_get(…) = 0;
  virtual reply http_post(…);
  virtual reply http_delete(…);
  // etc.
};

这正是我的问题 - 我不知道哪一个会被覆盖,但我想强制至少覆盖一个。 - Karl von Moor
@Karl von Moor:将其抽象化可以防止实例化,但可能与您的设计不兼容。 - mdec
“我不希望它成为可能”本身并不是一个很好的理由。这样做有什么害处吗?你是想防止实际的错误,还是只是试图阻止你看不到意义的操作?后者既让用户感到恼火,也从根本上是不可能的。前者是有用的,也是良好的实践。 - jalf
@jalf 为什么把没有意义的东西留在外面是不好的? - Karl von Moor
@Karl:有两个原因。首先,你还不知道哪些情况是有意义的。你可能会惊讶于你的库最终被使用的方式,人们可能会发现在设计时你认为毫无意义的边角案例中实际上做某些事情是有意义的。然后用户会因为没有实际好处而对你感到恼火。其次,你将为库添加复杂性,但没有实际好处。明智的程序员不需要被告知要做什么是明智的。如果某件事是毫无意义的,他们就不会去做它。他们不需要编译器禁止它。 - jalf
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接