C++ 中包装器 类 的大小写相关可变性(例如 uint8_t 数组包装器)

case dependent mutability of wrapper classes in C++ (e.g. uint8_t array wrapper)

如何为

的引用对象(如在关联中,而不是组合中)定义包装器

我的具体问题: 我正在编写一个内部处理 POD uint8_t[] 数组的函数,但应该使用包装器 class like

与外界接口
class BufferWrapper
{
public:
    BufferWrapper(uint8_t* pau8, size_t ui) : m_pau8{pau8}, m_uiSize{ui} {}

    uint8_t& operator[](size_t ui) { return m_pau8[ui]; }
    const uint8_t& operator[](size_t ui) const { return m_pau8[ui]; }

    size_t length() const { return m_uiSize; }

    /* other convenience functions ... */
private:
    uint8_t* m_pau8;
    size_t   m_uiSize;
};

我写了一个方便的从uint8_t[SIZE]到BufferWrapper的转换模板函数。 (我知道它只适用于在编译时已知大小的数组。)

template<typename T> BufferWrapper wrapArray(T& t)
{
    return BufferWrapper(t, sizeof(t));
}

只要数组是可变的,这就可以很好地工作,但如果实际数据源是 const uint8_t[],则显然无法编译,因为调用 BufferWrapper 构造函数会丢弃 const源数组的性。

我想要的是一个引用 const uint8_t[]const BufferWrapper 对象,但不应隐式更改为非 const.

我想出了通过使用 const T& 参数类型重载函数并在内部使用 const_cast 来编译的代码。

template<typename T> const BufferWrapper wrapArray(const T& t)
{
    return BufferWrapper(const_cast<T&>(t), sizeof(t));
}

然而,这是一个糟糕的解决方案,因为在复制构造另一个对象时 const return 类型被删除,例如

BufferWrapper newObject = wrapArray(my_const_uint8_array);

编译,即使它不应该编译。

对于类似的问题,我找到了两种不同的解决方案:

你有更好的解决方案吗?

这是一个独立的工作示例

#include <cinttypes>
#include <cstddef>
#include <cstdio>
#include <cstring>

class BufferWrapper
{
public:
    BufferWrapper(uint8_t* pau8, size_t ui) : m_pau8{pau8}, m_uiSize{ui} {}
    void fill(uint8_t u8) { memset(m_pau8, u8, m_uiSize); }

    uint8_t& operator[](size_t ui) { return m_pau8[ui]; }
    const uint8_t& operator[](size_t ui) const { return m_pau8[ui]; }

    size_t length() const { return m_uiSize; }

    bool operator==(const BufferWrapper& rcco)
    {
        return (m_uiSize == rcco.m_uiSize) // size equality
        && (0 == memcmp(m_pau8, rcco.m_pau8, m_uiSize)); // and content equality
    }
private:
    uint8_t* m_pau8;
    size_t   m_uiSize;
};

// example for data consumer that accepts a BufferWrapper object
void readDataFromBuffer(const BufferWrapper& rcco)
{
    for(size_t ui=0; ui<rcco.length(); ++ui)
    {
        printf("%02x", rcco[ui]);
    }
    printf("\n");
}

// convenience function to capture length of arrays
// (only works on arrays, not on pointers -- I know)
template<typename T> BufferWrapper wrapArray(T& t)
{
    printf("BufferWrapper, ptr=%p, size=%zu\n", &t, sizeof(t));
    return BufferWrapper(t, sizeof(t));
}

template<typename T> const BufferWrapper wrapArray(const T& t)
{
    printf("const BufferWrapper, ptr=%p, size=%zu\n", &t, sizeof(t));
    return BufferWrapper(const_cast<T&>(t), sizeof(t));
}

int main()
{
    uint8_t au8[]            = { 0xde, 0xad, 0xbe, 0xef };
    constexpr uint8_t cau8[] = { 0xba, 0xaa, 0xad, 0xc0, 0xde };
    
    readDataFromBuffer(wrapArray(au8));
    readDataFromBuffer(wrapArray(cau8));

    // this should _not_ compile, as it casts away the const of the Buffer object
    BufferWrapper coFoo = wrapArray(cau8); 
    coFoo[0] = 0xde;
    coFoo[1] = 0xee;
    readDataFromBuffer(coFoo);

    return 0;
}

Output(可以看出实际上const变量的内容被改变了):

$ clang -Wall toy_example.cpp 
$ ./a.out 
BufferWrapper, ptr=0x7fff647544d4, size=4
deadbeef
const BufferWrapper, ptr=0x7fff647544cc, size=5
baaaadc0de
const BufferWrapper, ptr=0x7fff647544cc, size=5
deeeadc0de

通常所做的是为非 const 包装器和 const 设置不同的类型。在您的情况下,名称可能是 BufferWrapperConstBufferWrapper,其中后者包含指向 const 的指针而不是指向非常量的指针。前者 class 可以隐式转换为后者。使用模板,甚至不需要任何重复。


P.S。 %02xstd::uint8_t 的无效格式说明符,因此程序的行为未定义(除了 UB 修改常量缓冲区)。同样 %pconst unsigned char (*)[N].

的无效格式说明符

P.P.S。该标准有一个通用包装器,例如您的 BufferWrapper 自 C++20 起名为 std::span。然而,它确实共享 BufferWrapper 的属性,即它不会将常量传播到指向的对象。

因此,您可以使用预先存在的 span 实现:

void readDataFromBuffer(std::span<const std::uint8_t> rcco) {
    for(unsigned u : rcco) { // note the correct type for %02x
        std::printf("%02x", u);
    }
    std::printf("\n");
}

// ...

readDataFromBuffer(au8);  // OK
readDataFromBuffer(cau8); // OK

这两个都无法按要求编译:

std::span<const std::uint8_t> coFoo = cau8; // OK
coFoo[0] = 0xde;                            // does not compile

// alternative
std::span<std::uint8_t> coFoo = cau8;       // does not compile
coFoo[0] = 0xde;                            // would have been OK

eerorika 的回答实际上已经解决了这个问题——我也会使用 C++20 中的 std::span

作为参考,我仍在努力更新示例代码,首先展示完整的工作代码,其次解决以下问题:

  • 使用重载的转换运算符自动从非 const 转换为 const 引用类型 operator TArrayWrapper<const U>()
  • 我使用的 BufferWrapper class 有一些方便的函数(此处:fill())或仅对非​​常量引用类型有意义的 to-const 类型转换运算符.我不确定使用模板 classes 是否有效,其中某些类型仅支持给定方法的一个子集。这就是为什么我想尝试 eran 的建议,即使用 std::enable_if 来有条件地包含某些方法。
  • (在没有 C++20 支持的情况下在编译器上运行的东西)

作为旁注: 我将名称 BufferWrapper 替换为 TArrayWrapper(并且将 memcmp/memset 替换为显式循环,这样它也允许对其他类型进行通用使用)以表明它可以在数组上工作uint8_t 以外的类型。

#include <cinttypes>
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <type_traits>

template<typename T> class TArrayWrapper
{
public:
    TArrayWrapper(T* pT, size_t ui) : m_pT{pT}, m_uiSize{ui} {}

    template<typename U = T, typename std::enable_if<std::is_const<U>::value == false, int>::type = 0>
    void fill(T t)
    {
        for(size_t ui=0; ui<m_uiSize; ++ui)
        {
            m_pT[ui] = t;
        }
    }

    template<typename U = T, typename std::enable_if<std::is_const<U>::value == false, int>::type = 0>
    T& operator[](size_t ui) { return m_pT[ui]; }

    const T& operator[](size_t ui) const { return m_pT[ui]; }

    template<typename U = T, typename std::enable_if<std::is_const<U>::value == false, int>::type = 0>
    operator TArrayWrapper<const U>() { return TArrayWrapper<const U>(m_pT, m_uiSize); }

    size_t length() const { return m_uiSize; }

    bool operator==(const TArrayWrapper<T>& rcco)
    {
        if(m_uiSize != rcco.m_uiSize)
        {
            return false;
        }
        for(size_t ui=0; ui<m_uiSize; ++ui)
        {
            if (m_pT[ui] != rcco.m_pT[ui])
            {
                return false;
            }
        }
        return true;
    }
private:
    T*     m_pT;
    size_t m_uiSize;
};

using ConstBufferWrapper = TArrayWrapper<const uint8_t>;
using BufferWrapper      = TArrayWrapper<uint8_t>;

void readDataFromBuffer(const ConstBufferWrapper& rcco)
{
    for(size_t ui=0; ui<rcco.length(); ++ui)
    {
        printf("%02" PRIx8, rcco[ui]);
    }
    printf("\n");
}

// convenience function to capture length of arrays
template<typename T> TArrayWrapper<typename std::remove_extent<T>::type> wrapArray(T& t)
{
    printf("TArrayWrapper, ptr=%p, size=%zu\n", static_cast<const void*>(&t), sizeof(t));
    return TArrayWrapper<typename std::remove_extent<T>::type>(t, sizeof(t));
}

int main()
{
    uint8_t au8[]            = { 0xde, 0xad, 0xbe, 0xef };
    constexpr uint8_t cau8[] = { 0xba, 0xaa, 0xad, 0xc0, 0xde };

    readDataFromBuffer(wrapArray(au8));
    readDataFromBuffer(wrapArray(cau8));

    BufferWrapper coTest = wrapArray(au8);
    coTest.fill(0xff);
    readDataFromBuffer(coTest);

    ConstBufferWrapper coFoo = wrapArray(cau8);
    //coFoo[0] = 0xde; // does not compile, as desired:
    //coFoo[1] = 0xee; // coFoo shall not allow write access to referenced buffer
    readDataFromBuffer(coFoo);

    return 0;
}