在C++中,关于位移和转换数据类型

Jos*_*ion 5 c++ sockets bit-manipulation opcode

我最近在Stack Overflow上问了一个关于如何将我的数据从16位整数后跟未定量的void*-cast内存转换为无符号字符的std :: vector的问题,以便使用已知的套接字库作为NetLink使用其签名如下所示的函数来发送原始数据:

void rawSend(const vector<unsigned char>* data);
Run Code Online (Sandbox Code Playgroud)

(供参考,这里是那个问题:将unsigned int +一个字符串转换为unsigned char向量)

这个问题得到了成功的回答,我很感谢那些回应的人.Mike DeSimone回复了一个send_message()函数示例,该函数将数据转换为NetLink接受的格式(std :: vector),如下所示:

void send_message(NLSocket* socket, uint16_t opcode, const void* rawData, size_t rawDataSize)
{
    vector<unsigned char> buffer;
    buffer.reserve(sizeof(uint16_t) + rawDataSize);
    buffer.push_back(opcode >> 8);
    buffer.push_back(opcode & 0xFF);
    const unsigned char* base(reinterpret_cast<const unsigned char*>(rawData));
    buffer.insert(buffer.end(), base, base + rawDataSize);
    socket->rawSend(&buffer);
}
Run Code Online (Sandbox Code Playgroud)

这看起来正是我所需要的,所以我开始编写一个附带的receive_message()函数......

...但是我很尴尬地说我并不完全理解所有的位移和诸如此类的东西,所以我在这里遇到了一堵墙.在我过去近十年编写的所有代码中,我的大部分代码都是使用更高级的语言,而我的其余代码并没有真正调用低级内存操作.

回到编写receive_message()函数的主题,我的起点,如您所想,是NetLink的rawRead()函数,其签名如下所示:

vector<unsigned char>* rawRead(unsigned bufferSize = DEFAULT_BUFFER_SIZE, string* hostFrom = NULL);
Run Code Online (Sandbox Code Playgroud)

看起来我的代码将开始这样的事情:

void receive_message(NLSocket* socket, uint16_t* opcode, const void** rawData)
{
    std::vector<unsigned char, std::allocator<unsigned char>>* buffer = socket->rawRead();
    std::allocator<unsigned char> allocator = buffer->get_allocator(); // do I even need this allocator?  I saw that one is returned as part of the above object, but...
    // ...
}
Run Code Online (Sandbox Code Playgroud)

在第一次调用rawRead()之后,看起来我需要迭代向量,从中检索数据并反转位移操作,然后将数据返回到*rawData和*操作码.再一次,我不太熟悉bitshifting(我做了一些谷歌搜索来理解语法,但我不明白为什么上面的send_message()代码需要移动),所以我对我的下一步感到茫然这里.

有人可以帮我理解如何编写这个伴随的receive_message()函数吗?作为奖励,如果有人可以帮助解释原始代码,以便我知道它的未来如何运作(特别是,在这种情况下如何转移以及为什么有必要),这将有助于加深我对未来的理解.

提前致谢!

Che*_*Alf 3

库的函数签名...

    void rawSend( const vector<unsigned char>* data );
Run Code Online (Sandbox Code Playgroud)

迫使您构建std::vector自己的数据,这本质上意味着它会带来不必要的低效率。要求客户端代码构建std::vector. 无论是谁设计的,都不知道他们在做什么,最好不要使用他们的软件。

库函数签名...

    vector<unsigned char>* rawRead(unsigned bufferSize = DEFAULT_BUFFER_SIZE, string* hostFrom = NULL);
Run Code Online (Sandbox Code Playgroud)

更糟糕的是:如果您想指定“hostFrom”(无论其真正含义是什么),它不仅不必要地要求您构建 a std::string,而且还不必要地要求您取消分配结果vector。至少对函数结果类型有任何意义。当然,也可能没有。

您不应该使用具有如此令人厌恶的函数签名的库。也许任何随机挑选的库都会好得多。即,更容易使用。


现有的使用代码如何...

void send_message(NLSocket* socket, uint16_t opcode, const void* rawData, size_t rawDataSize)
{
    vector<unsigned char> buffer;
    buffer.reserve(sizeof(uint16_t) + rawDataSize);
    buffer.push_back(opcode >> 8);
    buffer.push_back(opcode & 0xFF);
    const unsigned char* base(reinterpret_cast<const unsigned char*>(rawData));
    buffer.insert(buffer.end(), base, base + rawDataSize);
    socket->rawSend(&buffer);
}
Run Code Online (Sandbox Code Playgroud)

作品:

  • reserve调用是过早优化的一个例子。它尝试vector只执行一个缓冲区分配(此时执行),而不是两个或多个。解决构建 的明显低效问题的一个更好的方法vector是使用更健全的库。

  • buffer.push_back(opcode >> 8)(假定的)16 位数量 的高 8 位放置opcode在向量的开头。首先放置高位部分,即最重要的部分,称为大端格式。另一端的读取代码必须采用大端格式。同样,如果此发送代码使用小端格式,则读取代码也必须采用小端格式。因此,这只是一个数据格式决定,但鉴于该决定,两端的代码都必须遵守它。

  • 调用buffer.push_back(opcode & 0xFF)将低 8 位放在opcode高位之后,这对于大端来说是正确的。

  • const unsigned char* base(reinterpret_cast<const unsigned char*>(rawData))声明只是命名一个指向数据的适当类型的指针,并将其称为base. 该类型const unsigned char*很合适,因为它允许字节级地址算术。原始的形式参数类型const void*不允许地址算术。

  • buffer.insert(buffer.end(), base, base + rawDataSize)数据添加到向量中。该表达式base + rawDataSize是先前声明启用的地址算术。

  • socket->rawSend(&buffer)是对 SillyLibrary 方法的最终调用rawSend


如何包装对 SillyLibraryrawRead函数的调用。

首先,为字节数据类型定义一个名称(命名总是一个好主意):

typedef unsigned char Byte;
typedef ptrdiff_t Size;
Run Code Online (Sandbox Code Playgroud)

请查阅有关如何解除分配/销毁/删除(如有必要)SillyLibrary 函数结果的文档:

void deleteSillyLibVector( vector<Byte> const* p )
{
    // perhaps just "delete p", but it depends on the SillyLibrary
}
Run Code Online (Sandbox Code Playgroud)

现在,对于涉及的发送操作来说std::vector只是一种痛苦。对于接收操作,则相反。创建动态数组并将其作为函数结果安全有效地传递,这正是std::vector设计的目的。

然而,发送操作只是一次调用。

对于接收操作根据 SillyLibrary 的设计,您可能需要循环执行多次接收调用,直到收到所有数据。您没有提供足够的信息来执行此操作。但下面的代码显示了循环代码可以调用的底层读取vector,将数据累积在 a 中:

Size receive_append( NLSocket& socket, vector<Byte>& data )
{
    vector<Byte> const* const result = socket.raw_read();

    if( result == 0 )
    {
        return 0;
    }

    struct ScopeGuard
    {
        vector<Byte>* pDoomed;
        explicit ScopeGuard( vector<Byte>* p ): pDoomed( p ) {}
        ~ScopeGuard() { deleteSillyLibVector( pDoomed ); }
    };

    Size const nBytesRead = result->size();
    ScopeGuard cleanup( result );

    data.insert( data.end(), result->begin(), result->end() );
    return nBytesRead;
}
Run Code Online (Sandbox Code Playgroud)

请注意使用析构函数进行清理,这使得此异常更加安全。在这种特殊情况下,唯一可能的例外是 a std::bad_alloc,无论如何,这都是相当致命的。但是,为了异常安全,使用析构函数进行清理的一般技术非常值得了解和使用(尽管通常不必定义任何新类,但是在处理 SillyLibrary 时可能必须这样做)。

最后,当您的循环代码确定所有数据都已准备就绪时,它可以解释vector. 我将其作为练习,尽管这主要是您所要求的。那是因为我已经在这里写了几乎整篇文章。

免责声明:即兴代码。

干杯,