问题
I am new to C++. I found that the following programming style is quite interesting to me. I wrote a simplified version here.
#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;
}
The example here is compilable, but I didn't run it because I must go into an infinite loop. But, I hope to use this as a reference. The read() and write() are calling each other. I first encountered this programming style in boost.asio. When the server received a message in do_read(), it calls do_write() to echo the client, then it calls do_read() again at the end of the do_write().
I have two questions regarding this type of coding.
Will this cause stack overflow? Because the functions are keeping calling themselves and the function ends only an error occurs.
What is the advantage of it? Why can't I use a function to loop them orderly and break the loop whenever it encounters an error.
bool replied = true; while (!error) { if (replied) read(); else { write(); replied = !replied; } }
回答1:
Your simplified version leaves out the most important aspect: the write()
and read()
calls are asynchronous.
Therefore, the functions don't actually cause recursion, see this recent answer: Do "C++ boost::asio Recursive timer callback" accumulate callstack?
The "unusual" thing about async_read(...)
and async_write(...)
is that the functions return before the IO operation has actually been performed, let alone completed. The actual execution is done on a different schedule¹.
To signal compleion back to the "caller" the async calls typically take a completion handler, which gets called with the result of the IO operation.
In that completion handler, it's typical to see either the end of the communication channel, or the next IO operation being scheduled. This is known as asynchronous call chaining and is very prominently present in many languages that support asynchronous operations ²
It takes some getting used to, but ultimately you get used to the pattern.
With this in mind, revisit one of the boost samples and see if the penny drops:
Documentation sample Chat Client
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();
}
Benefit Of Asynchronous Operations
Asynchronous IO are useful for a more event-based model of IO. Also they remove the first "ceiling" when scaling to large volumes of IO operations. In traditional, imperative code patterns many clients/connections would require many threads in order to be able to serve them simultaneously. In practice, though, threads fail to scale (since a typical server has a smallish number of logical CPUs) and it would mean that IO operations block each other ³.
With asynchronous IO you can often do all IO operations on a single thread, greatly improving efficiency - and thereby some aspects of the program design (because fewer threading issues need to be involved).
¹ Many choices exist, but imagine that io_service::run()
is running on a separate thread, that would lead to the IO operations being actually executed, potentially resumed when required and completed on that thread
² I'd say javascript is infamous for this pattern
³ A classical example is when a remote procedure call keeps a thread occupied while waiting for e.g. a database query to complete
回答2:
This is my opinion:
Regarding recursion
One way to cause a stack overflow is to have a function calling itself recursively, overflowing the call stack. A set of functions calling each other in a circular manner would be equivalent to that, so yes, your intuition is correct.
An iterative version of the algorithm, such as the loop you describe, could prevent that.
Now, another thing that can prevent a stack overflow is the presence of code that could be optimized for tail recursion. Tail recursion optimization requires a compiler implementing this feature. Most major compilers implement it. The Boost.Asio function you mention seems to be benefiting from this optimization.
Regarding code design
Now, C++ implements many programming paradigms. These paradigms are also implemented by many other programming languages. The programming paradigms relevant to what you are discussing would be:
- Structured programming
- Object oriented programming
From a structured programming point of view, you should try to emphasize code reuse as much as possible by diving the code in subroutines that minimize redundant code.
From an object oriented point of view, you should model classes in a way that encapsulates their logic as much as possible.
The logic you present so far seems encapsulated enough, however, you may need to review if the methods write
and read
should remain public
, or if they should be private
instead. Minimizing the number of public methods helps achieving a higher level of encapsulation.
来源:https://stackoverflow.com/questions/43884931/what-is-the-advantage-of-class-member-functions-to-call-each-other-in-c