Why do we use char* as a buffer, why not a string in boost::asio?

Viewed 636

Recently I have been reading a book about network programming with boost::asio, and from what I have understood, buffer is like any other memory space in address space of the program and we assign that to the socket so that we can do I/O operations.

The first thing I don't understand is, why do we need a separate thing called "buffer"? Why not just write the content in a string, and then when we receive put it in the string?

The second thing I don't understand is, why is char* or char[] used as a buffer, why not int[], which can store ASCII values of everything that comes through? It's just a memory, after all. I feel like I'm missing something here, please help me out.

Thirdly, why can't C++ std::string be used as a buffer? Every time, they have to be converted to C strings.

3 Answers

I think both the answers, by giving arguments against string or int[] miss the general point:

Boost Doesn't Make That Choice For You

In other words

You Are Free To Use All Of These To Your Taste

Demo Live On Coliru:

#include <boost/asio.hpp>
#include <iostream>

template <typename Buffer>
size_t test_request(Buffer response) {
    using boost::asio::ip::tcp;

    boost::asio::io_context io;
    tcp::socket s(io);
    s.connect({ boost::asio::ip::address_v4{{1,1,1,1}}, 80 }); 

    write(s, boost::asio::buffer("GET / HTTP/1.1\r\n"
         "Host: 1.1.1.1\r\n"
         "Referer: stoackoverflow.com\r\n"
         "\r\n"));

    boost::system::error_code ec;
    auto bytes = read(s, response, ec);
    std::cerr << "test_request: " << ec.message() << " at " << bytes << " bytes\n";

    return bytes;
}

#include <iomanip>
int main() {
    std::string s;
    std::vector<char> vec(4096);
    int ints[1024];

    {
        auto n = test_request(boost::asio::buffer(vec));
        vec.resize(n);
    }

    // or use the ints[]
    test_request(boost::asio::buffer(ints));

    // use a dynamic buffer (that grows):
    test_request(boost::asio::dynamic_buffer(s));

    auto report = [](std::string_view sv) {
        std::cout << sv.length() << " bytes\n"
            << " first: " << std::quoted(sv.substr(0, sv.find_first_of("\r\n"))) << "\n"
            << " last:  " << std::quoted(sv.substr(sv.find_last_of("\r\n", sv.size()-3)+1)) << "\n";
    };

    std::cout << "String response: "; report(s);
    std::cout << "Vector response: "; report({vec.data(), vec.size()});
}

Prints

test_request: End of file at 909 bytes
test_request: End of file at 909 bytes
test_request: End of file at 909 bytes
String response: 909 bytes
 first: "HTTP/1.1 301 Moved Permanently"
 last:  "</html>
"
Vector response: 909 bytes
 first: "HTTP/1.1 301 Moved Permanently"
 last:  "</html>
"

Summary

The whole point is not opinionism on text encodings or whatnot¹.

The whole point is

  • Don't Pay For What You Don't Use (extra conversions take allocations and just cost performance)
  • Non-Intrusive Framework (the framework should not dictate what vocabulary types you must use)

¹ (std::string is suitable for, say UTF8 or ASCII7 or indeed binary data - it will handle NUL chars just fine).

why not just write the content in a string, and then when we receive it put in the string.

Because Boost ASIO is a library for binary IO; not for textual IO. And std::string is for representing text. Technically, you could use a std::string as a buffer for binary data, but doing so would be unconventional and confusing.

why not int[]

Because the narrow character types are special in the C++ language.

Generally objects of one type cannot be observed as objects of another type. For example, if you have a short object and a long long object and want to send those over the network, you cannot "observe" at those objects as being (arrays) of int objects, because they aren't int obects. But, every object can be "observed" (through reinterpretation) as arrays of narrow character objects. That is the unique feature of char, unsigned char and std::byte which is why they are used as buffers for serialisation. And also that their size is exactly one byte which the fundamental storage unit in the C++ memory model.

which can store ASCII value

Which is largely irrelevant in binary IO since ASCII is a text encoding. It would also be quite wasteful to use 16 bits (at minimum; 32 bits on most systems) to represent ASCII that is a 7 bit encoding.

What you're missing is the fact that the content of a buffer might not actually represent a string, it's just a block of memory that could represent anything. That should also explain why std::string shouldn't be used as a buffer.

The reason why char is used as a type is that it (usually) has a size of one byte, so the buffer is actually just an array of bytes, and having char as the type allows easy per-byte manipulation of that memory (like, for example, copying a block of memory at a specific byte offset into/out of the buffer, etc).

Related