类成员函数在C++中相互调用有什么优势?

What is the advantage of class member functions to call each other in C++?

本文关键字:调用 什么 函数 成员 C++      更新时间:2023-10-16

我是C++新手。我发现下面的编程风格对我来说很有趣。我在这里写了一个简化版本。

#include <iostream>
using namespace std;
class MyClass {
public :
MyClass(int id_) : id(id_) {
cout<<"I am a constructor"<<endl;
}
bool error = false;
void run() {
//do something ...
if (!error) {
read();
}
}
void read() {
//do something ...
if (!error) {
write();
}
}
void write() {
//do something ...
if (!error) {
read();
}
}
private :
int id;
};
int main() {
MyClass mc(1);  
mc.run();
return 0;
}

这里的例子是可编译的,但我没有运行它,因为我必须进入无限循环。但是,我希望以此为参考。read() 和 write() 相互调用。我第一次遇到这种编程风格是在boost.asio。当服务器收到 do_read() 中的消息时,它会调用 do_write() 来回显客户端,然后在 do_write() 的末尾再次调用 do_read()。

关于这种类型的编码,我有两个问题。

  1. 这会导致堆栈溢出吗?因为函数不断调用自己,而函数结束时只会发生错误。

  2. 它有什么好处?为什么我不能使用函数来有序地循环它们,并在遇到错误时中断循环。

    bool replied = true;
    while (!error) {
    if (replied) read();
    else {
    write();
    replied = !replied;
    }
    }
    

您的简化版本省略了最重要的方面:write()read()调用是异步的

因此,这些函数实际上并不会导致递归,请参阅最近的答案:"C++ boost::asio 递归计时器回调"会累积调用堆栈吗?

async_read(...)async_write(...)的"不寻常"之处在于,函数在实际执行IO操作之前返回,更不用说完成了。实际执行按不同的计划¹完成。

为了向"调用方"发出 compleion 信号,异步调用通常采用完成处理程序,该处理程序使用 IO 操作的结果进行调用。

在该完成处理程序中,通常会看到通信通道的结束或正在计划的下一个 IO 操作。这称为异步调用链,在许多支持异步操作的语言中都非常突出 ²

这需要一些时间来适应,但最终你会习惯这种模式。

考虑到这一点,重新访问其中一个提升样本,看看一分钱是否下降:

文档示例聊天客户端

void handle_connect(const boost::system::error_code& error)
{
if (!error)
{
boost::asio::async_read(socket_,
boost::asio::buffer(read_msg_.data(), chat_message::header_length),
boost::bind(&chat_client::handle_read_header, this,
boost::asio::placeholders::error));
}
}
void handle_read_header(const boost::system::error_code& error)
{
if (!error && read_msg_.decode_header())
{
boost::asio::async_read(socket_,
boost::asio::buffer(read_msg_.body(), read_msg_.body_length()),
boost::bind(&chat_client::handle_read_body, this,
boost::asio::placeholders::error));
}
else
{
do_close();
}
}
void handle_read_body(const boost::system::error_code& error)
{
if (!error)
{
std::cout.write(read_msg_.body(), read_msg_.body_length());
std::cout << "n";
boost::asio::async_read(socket_,
boost::asio::buffer(read_msg_.data(), chat_message::header_length),
boost::bind(&chat_client::handle_read_header, this,
boost::asio::placeholders::error));
}
else
{
do_close();
}
}
void do_write(chat_message msg)
{
bool write_in_progress = !write_msgs_.empty();
write_msgs_.push_back(msg);
if (!write_in_progress)
{
boost::asio::async_write(socket_,
boost::asio::buffer(write_msgs_.front().data(),
write_msgs_.front().length()),
boost::bind(&chat_client::handle_write, this,
boost::asio::placeholders::error));
}
}
void handle_write(const boost::system::error_code& error)
{
if (!error)
{
write_msgs_.pop_front();
if (!write_msgs_.empty())
{
boost::asio::async_write(socket_,
boost::asio::buffer(write_msgs_.front().data(),
write_msgs_.front().length()),
boost::bind(&chat_client::handle_write, this,
boost::asio::placeholders::error));
}
}
else
{
do_close();
}
}
void do_close()
{
socket_.close();
}

异步操作的好处

异步 IO 对于更基于事件的 IO 模型非常有用。此外,它们在扩展到大量 IO 操作时消除了第一个"上限"。在传统的命令式代码模式中,许多客户端/连接需要许多线程才能同时为它们提供服务。但实际上,线程无法扩展(因为典型的服务器具有少量的逻辑 CPU),这意味着 IO 操作会相互阻塞³。

使用异步 IO,您通常可以在单个线程上执行所有 IO 操作,从而大大提高了效率 - 从而提高了程序设计的某些方面(因为需要涉及的线程问题更少)。


¹ 存在许多选择,但假设io_service::run()在单独的线程上运行,这将导致 IO 操作实际执行,在需要时可能恢复并在该线程上完成

² 我会说 javascript 因这种模式而臭名昭著

³ 一个经典的例子是,当远程过程调用在等待数据库查询完成时保持线程占用

这是我的观点:

关于递归

导致堆栈溢出的一种方法是让函数递归调用自身,使调用堆栈溢出。一组以循环方式相互调用的函数等效于此,所以是的,您的直觉是正确的。

算法的迭代版本(例如您描述的循环)可以防止这种情况。

现在,可以防止堆栈溢出的另一件事是存在可以针对尾递归进行优化的代码。尾递归优化需要实现此功能的编译器。大多数主要编译器都实现了它。您提到的Boost.Asio函数似乎受益于这种优化。

关于代码设计

现在,C++实现了许多编程范例。这些范式也由许多其他编程语言实现。与您正在讨论的内容相关的编程范例是:

  • 结构化编程
  • 面向对象编程

从结构化编程的角度来看,您应该尝试通过在最小化冗余代码的子例程中潜水代码来尽可能强调代码重用。

从面向对象的角度来看,您应该以尽可能封装其逻辑的方式对类进行建模。

到目前为止,您提出的逻辑似乎已经足够封装,但是,您可能需要查看方法writeread是否应保持public,或者是否应该private它们。最小化公共方法的数量有助于实现更高级别的封装。