数据结构
class uint128_fallback {private:uint64_t lo_, hi_;public:constexpr uint128_fallback(uint64_t hi, uint64_t lo) : lo_(lo), hi_(hi) {}constexpr uint128_fallback(uint64_t value = 0) : lo_(value), hi_(0) {}constexpr uint64_t high() const noexcept { return hi_; }constexpr uint64_t low() const noexcept { return lo_; }template <typename T, FMT_ENABLE_IF(std::is_integral<T>::value)>constexpr explicit operator T() const {return static_cast<T>(lo_);}friend constexpr auto operator==(const uint128_fallback& lhs,const uint128_fallback& rhs) -> bool {return lhs.hi_ == rhs.hi_ && lhs.lo_ == rhs.lo_;}friend constexpr auto operator!=(const uint128_fallback& lhs,const uint128_fallback& rhs) -> bool {return !(lhs == rhs);}friend constexpr auto operator>(const uint128_fallback& lhs,const uint128_fallback& rhs) -> bool {return lhs.hi_ != rhs.hi_ ? lhs.hi_ > rhs.hi_ : lhs.lo_ > rhs.lo_;}
加法
注意处理进位
FMT_CONSTEXPR void operator+=(uint128_fallback n) {uint64_t new_lo = lo_ + n.lo_;uint64_t new_hi = hi_ + n.hi_ + (new_lo < lo_ ? 1 : 0);FMT_ASSERT(new_hi >= hi_, "");lo_ = new_lo;hi_ = new_hi;}friend auto operator-(const uint128_fallback& lhs, uint64_t rhs)-> uint128_fallback {return {lhs.hi_ - (lhs.lo_ < rhs ? 1 : 0), lhs.lo_ - rhs};}
乘法
这里作者为了简单,只处理了和u32相乘的情形,不过程序本身就是只满足当前需求即可,不用盲目应对所有的事情,简单是一种美。
friend auto operator*(const uint128_fallback& lhs, uint32_t rhs)-> uint128_fallback {FMT_ASSERT(lhs.hi_ == 0, "");uint64_t hi = (lhs.lo_ >> 32) * rhs;uint64_t lo = (lhs.lo_ & ~uint32_t()) * rhs;uint64_t new_lo = (hi << 32) + lo;return {(hi >> 32) + (new_lo < lo ? 1 : 0), new_lo};}
关于乘法原理,我一开始还没看懂,后来发现,这里的代码是针对u64是否进位,因为乘法的主体就是u64的数字。附上一段有趣的对话。
Me
可以介绍一下这段乘法的计算原理吗Assistant
当我们对一个 uint128_fallback 对象的低64位 lhs.lo_ 与一个 uint32_t 值 rhs 进行乘法运算时,可以将乘法分解为两个部分:高位部分和低位部分。首先,我们计算 lhs.lo_ 右移32位后的值与 rhs 的乘积,这部分乘积的结果是一个64位的值,存储在 hi 变量中。这个乘积的结果是原始乘法的高位部分,即 (lhs.lo_ >> 32) * rhs。接下来,我们计算 lhs.lo_ 与 ~uint32_t()(即将 uint32_t 的所有位取反)进行按位与的结果与 rhs 的乘积。这部分乘积的结果也是一个64位的值,存储在 lo 变量中。这个乘积的结果是原始乘法的低位部分,即 (lhs.lo_ & ~uint32_t()) * rhs。然后,我们将高位部分 hi 左移32位(相当于乘以 2^32)并与低位部分 lo 相加,得到一个128位的值 new_lo。这个值包含了原始乘法的结果的低64位。最后,我们根据进位情况来计算结果的高位部分 hi。如果 new_lo 小于 lo,说明低位部分的加法产生了进位,需要在 hi 的基础上加1。否则,进位为0,直接将 hi 的值赋给结果的高位部分。最终,我们将计算得到的 hi 和 new_lo 构造成一个新的 uint128_fallback 对象并返回。这个对象包含了原始乘法的结果的高64位和低64位。Me
为什么不计算lhs.hi_呢 2. 进位那里我还是没懂
Assistant
在给定的代码中,乘法运算只涉及到了 lhs 对象的低64位 lhs.lo_,并且在断言 FMT_ASSERT(lhs.hi_ == 0, ""); 中要求 lhs.hi_ 的值为0。因此,这段代码的设计可能是基于特定的使用场景和需求,只关注低64位的乘法运算。如果需要对整个128位进行乘法运算,就需要考虑 lhs.hi_ 的值。关于进位的处理,我们需要考虑低64位乘法运算的溢出情况。在这里,我们使用了一个简单的技巧来判断低位部分的加法是否产生了进位。具体来说,我们比较 new_lo 和 lo 的大小。如果 new_lo 小于 lo,说明低位部分的加法产生了进位。这是因为在加法运算中,如果加数和被加数的最高位都为1时,会产生进位。因此,我们在高位部分 hi 的基础上加1,以表示进位的存在。如果 new_lo 大于等于 lo,则说明没有发生进位,进位为0,直接将 hi 的值赋给结果的高位部分。这种处理方式是一种简单的处理进位的方法,适用于这个特定的乘法运算场景。在其他情况下,可能需要更复杂的进位处理逻辑,具体取决于具体的算法和需求。Me
哦,我想明白了,这里的进位是针对uint64_t的进位!
Assistant
是的,你理解得对!这里的进位确实是针对 uint64_t 类型的进位。由于我们在计算乘法结果时使用了两个 uint64_t 类型的变量 hi 和 lo 来保存乘法的高位和低位部分,因此在处理进位时也是针对这两个变量进行的。具体来说,在计算新的低位部分 new_lo 时,我们将 hi 左移了32位,并将其与 lo 相加。这个加法操作可能会产生进位,即在加法过程中产生了更高位的进位。我们在这里通过比较 new_lo 和 lo 的大小来判断是否发生了进位。如果 new_lo 小于 lo,说明产生了进位,我们需要在高位部分 hi 的基础上加1,以表示进位的存在。由于 hi 和 lo 都是 uint64_t 类型,因此进位的处理是基于这两个64位变量的运算。这种进位处理方式适用于处理64位无符号整数的进位情况。
位移
FMT_CONSTEXPR auto operator>>(int shift) const -> uint128_fallback {if (shift == 64) return {0, hi_};if (shift > 64) return uint128_fallback(0, hi_) >> (shift - 64);return {hi_ >> shift, (hi_ << (64 - shift)) | (lo_ >> shift)};}FMT_CONSTEXPR auto operator<<(int shift) const -> uint128_fallback {if (shift == 64) return {lo_, 0};if (shift > 64) return uint128_fallback(lo_, 0) << (shift - 64);return {hi_ << shift | (lo_ >> (64 - shift)), (lo_ << shift)};}
位拷贝
这里大佬使用了模板选择的方式,先给一个使用案例,再来理解大佬的代码
struct uint32_pair {uint32_t u[2];
};TEST(util_test, bit_cast) {auto s = fmt::detail::bit_cast<uint32_pair>(uint64_t{42});EXPECT_EQ(fmt::detail::bit_cast<uint64_t>(s), 42ull);s = fmt::detail::bit_cast<uint32_pair>(~uint64_t{0});EXPECT_EQ(fmt::detail::bit_cast<uint64_t>(s), ~0ull);
}
好的,大佬这里的注释写道是做位拷贝的,对于一个To大于From长度的类型,先做存储空间的扩容,扩充到长度一样的情况之后再拷贝,如果4字节扩容到比From大,则编译失败。
// A heterogeneous bit_cast used for converting 96-bit long double to uint128_t
// and 128-bit pointers to uint128_fallback.
template <typename To, typename From, FMT_ENABLE_IF(sizeof(To) > sizeof(From))>
inline auto bit_cast(const From& from) -> To {constexpr auto size = static_cast<int>(sizeof(From) / sizeof(unsigned));struct data_t {unsigned value[static_cast<unsigned>(size)];} data = bit_cast<data_t>(from);auto result = To();if (const_check(is_big_endian())) {for (int i = 0; i < size; ++i)result = (result << num_bits<unsigned>()) | data.value[i];} else {for (int i = size - 1; i >= 0; --i)result = (result << num_bits<unsigned>()) | data.value[i];}return result;
}// Implementation of std::bit_cast for pre-C++20.
template <typename To, typename From, FMT_ENABLE_IF(sizeof(To) == sizeof(From))>
FMT_CONSTEXPR20 auto bit_cast(const From& from) -> To {
#ifdef __cpp_lib_bit_castif (is_constant_evaluated()) return std::bit_cast<To>(from);
#endifauto to = To();// The cast suppresses a bogus -Wclass-memaccess on GCC.std::memcpy(static_cast<void*>(&to), &from, sizeof(to));return to;
}
这里,我一开始对于bit_cast
理解错误,以为存在了递归调用。然后自己写了一个错的例子,果然就递归调用了,事实上大佬使用了std::enable_if_t< sizeof To > sizeof From>
来避免了递归,因为调用后就会转到std::enable_if_t< sizeof To == sizeof From>
的调用上。
附一个自己写的错误的例子,会递归调用导致编译失败。
#include <iostream>
#include <limits>
#include <type_traits>#define FMT_ENABLE_IF(T) std::enable_if_t<(T)>template <typename T>
constexpr auto num_bits() -> int
{return std::numeric_limits<T>::digits;
}template <typename To, typename From, typename = FMT_ENABLE_IF(sizeof(To) >= sizeof(From))>
inline auto bit_cast(const From& from) -> To {constexpr auto size = static_cast<int>(sizeof(From) / sizeof(unsigned));struct data_t {unsigned value[static_cast<unsigned>(size)];} data = bit_cast<data_t>(from);auto result = To();if (true) {for (int i = 0; i < size; ++i)result = (result << num_bits<unsigned>()) | data.value[i];} else {for (int i = size - 1; i >= 0; --i)result = (result << num_bits<unsigned>()) | data.value[i];}return result;
}struct uint32_pair {uint32_t u[2];
};int main()
{auto s = bit_cast<uint32_pair>(uint64_t{42});
}
最后,大佬应该是为了类型严谨,使用了模板的手法做预处理,确实更为安全,一般写法如下,弊病是会忽略大小端的差异,也会有内存越界的风险(考虑From的类型长度小于To的时候)。
template <typename To, typename From, typename = std::enable_if_t<sizeof(To) == sizeof(From)>>
inline auto bit_cast(const From& from) -> To {To result;std::memcpy(&result, &from, sizeof(To));return result;
}