c++ 如何拥有一个类似于unsigned char的类型,但不允许别名?

knsnq2tg  于 2023-10-20  发布在  其他
关注(0)|答案(3)|浏览(125)

我想有一个类型,就像unsigned char

  • sizeof是1
  • 整数值可以分配给它(没有任何类型转换)
  • 允许位操作
  • 算术是允许的,但不是必须的
  • 无符号
  • 可简单复制的

但是,与unsigned char不同,它不允许别名。我的意思是,一个类型,它没有异常[basic.lval/11.8]:
如果一个程序试图通过非以下类型的glvalue访问对象的存储值,则行为未定义:
[...]

  • char、unsigned char或std::字节类型。

有可能有这样的类型吗?
原因是:我几乎从不使用unsigned char的aliasing属性。所以,我想使用一种类型来代替,它不会阻止某些类型的优化(注意,我问这个问题是因为我实际上有一些函数,由于unsigned char的允许别名属性,这些函数没有很好地优化)。所以,我想有一个类型,这是真的:“不要为你不用的东西付钱”。
下面是一个例子,其中unsigned char阻止优化:Using this pointer causes strange deoptimization in hot loop

j8ag8udp

j8ag8udp1#

标准的这一部分调用了charunsigned charstd::byte。但是,你可以创建自己的类型,比如std::byte,它不允许别名:

enum class my_byte : unsigned char {};

使用它并不是那么好,因为你必须强制转换为unsigned char才能用它做任何有意义的事情。但是,您可以重载位运算符和算术运算符,使其更易于使用。
我们可以用下面的简单函数来验证这一点:

auto foo(A& a, B& b) {
    auto lhs = b;
    a = 42;
    auto rhs = b;
    return lhs + rhs;
}

如果A被允许与B别名,编译器将不得不生成两个加载:一个用于lhs,一个用于rhs。如果不允许AB别名,编译器可以生成一个单一的加载,并将值添加到自身。Let's test it

// int& cannot alias with long&
auto foo(int& a, long& b) {
    auto lhs = b;
    a = 42;
    auto rhs = b;
    return lhs + rhs;
}

// std::byte& can alias with long&    
auto bar(std::byte& a, long& b) {
   auto lhs = b;
    a = (std::byte)42;
    auto rhs = b;
    return lhs + rhs;
}

// if my_byte& can alias with long&, there would have to be two loads
auto baz(my_byte& a, long& b) {
    auto lhs = b;
    a = (my_byte)42;
    auto rhs = b;
    return lhs + rhs;
}

结果如下:

foo(int&, long&):
        mov     rax, QWORD PTR [rsi]
        mov     DWORD PTR [rdi], 42
        add     rax, rax
        ret
bar(std::byte&, long&):
        mov     rax, QWORD PTR [rsi]
        mov     BYTE PTR [rdi], 42
        add     rax, QWORD PTR [rsi]
        ret
baz(my_byte&, long&):
        mov     rax, QWORD PTR [rsi]
        mov     BYTE PTR [rdi], 42
        add     rax, rax
        ret

因此,my_byte不继承与charstd::byte相同的别名属性

w6mmgewl

w6mmgewl2#

您可以定义自己的类型:

#include <type_traits>

class uchar {
    unsigned char value = {};

public:
    template <typename T,
        std::enable_if_t<
            std::is_convertible_v<T, unsigned char>,
            int
        > = 0>
    constexpr uchar(T value)
        : value{static_cast<unsigned char>(value)}
    {}

    constexpr uchar()
    {}

    template <typename T,
        std::enable_if_t<
            std::is_convertible_v<T, unsigned char>,
            int
        > = 0>
    constexpr uchar& operator=(T value)
    {
        this->value = static_cast<unsigned char>(value);
        return *this;
    }

    explicit constexpr operator unsigned char() const
    {
        return value;
    }

    friend constexpr uchar operator+(uchar lhs, uchar rhs) {
        return lhs.value + rhs.value;
    }

    friend constexpr uchar operator-(uchar lhs, uchar rhs) {
        return lhs.value - rhs.value;
    }

    // And so on...
};

// The compiler could technically add padding after the `value` member of
// `uchar`, so we `static_assert` to verify that it didn't. I can't imagine
// any sane implementation would do so for a single-member type like `uchar`
static_assert(sizeof(uchar) == sizeof(unsigned char));
static_assert(alignof(uchar) == alignof(unsigned char));
mfpqipee

mfpqipee3#

C++的别名规则,包括它的“字符类型”例外,都是从C的那些规则衍生而来的,这些规则基于几个相对没有争议的原则:
1.给定一个结构,如:

int x;
 int test(double *p)
 {
   x=1;
   *p = 2.0;
   return x;
 }

不应该要求编译器适应*p可能标识与x相同的存储的可能性。
1.一个结构,如:

void dump_hex(void *dat, int len)
 {
     unsigned char *p = (char*)dat;
     for (int i=0; i<len; i++)
         printf("%02X", p[i]);
 }

应该可用于任何数据类型,并且应该可以编写可互换地对有符号和无符号数据进行操作的代码。
虽然标准没有强制要求这样做,但我认为大多数人都会同意,在char是8位的实现中,

void dump_hex2(void *dat, int len)
    {
        uint8_t *p = (char*)dat;
        for (int i=0; i<len; i++)
            printf("%02X", p[i]);
    }

应该和unsigned char版本一样好用。
该标准将允许一种实现,该实现能够支持至少模糊地类似于上述dump_hex2的结构,而不必悲观地适应与宇宙中任何地方的每个uint8_t*相关的所有场景,能够识别每种类型的每个可寻址对象的一部分,但不幸的是,它并不要求实现做出哪怕是最轻微的努力来支持这样的构造。尽管如此,这可能是最接近于与现有实现兼容的结构,但允许实现寻求有效地适应程序员需要执行的任务,以生成比其他方式更有效的代码。

相关问题