【问题标题】:Needing clarity on auto return type deduction for friend operator+ in a class template需要明确类模板中友元运算符+的自动返回类型扣除
【发布时间】:2019-07-14 07:30:19
【问题描述】:

我只是想弄清楚这段代码发生了什么以及为什么会这样:

ma​​in.cpp

#include <fstream>
#include <iostream>
#include <iomanip>
#include <sstream>

#include "Register.h"

int main() {
    using namespace vpc;

    Reg8 r8{ 0xEF };

    Reg16 expected{ 478 };
    Reg16 r16a = r8 + r8;
    Reg16 r16b{ r8 + r8 };
    std::cout << expected << r16a << r16b;

    return EXIT_SUCCESS;
}

我的 main 函数中的代码不会改变,因为这两种情况都是一样的:


这是我正在处理的operator+,这是我的第一次尝试:

template<typename Lhs, typename Rhs>
auto operator+(const Register<Lhs>& l, const Register<Rhs>& r) {
    auto tmp = l.value + r.value;
    if (sizeof(l.value) < sizeof(r.value))
        return Register<Rhs>{ tmp };
    else
        return Register<Lhs>{ tmp };
}

这是程序的输出:

输出 v1

Reg16(478)
Prev: 0
hex: 0x01DE
bin: 0000000111011110

Reg16(222)
Prev: 0
hex: 0x00DE
bin: 0000000011011110

Reg16(222)
Prev: 0
hex: 0x00DE
bin: 0000000011011110

正如您在上面看到的,期望值应该是十进制的478 或十六进制的0x01DE。但是,在这种情况下,operator=Register&lt;T&gt; 构造函数没有从 operator+ 获得适当的值。


我可以通过将我的operator+ 更改为此来解决此问题:

template<typename Lhs, typename Rhs>
auto operator+(const Register<Lhs>& l, const Register<Rhs>& r) {
    return Register<decltype(l.value + r.value)>{ l.value + r.value };
}

这给了我正确的结果:

输出 v2

Reg16(478)
Prev: 0
hex: 0x01DE
bin: 0000000111011110

Reg16(478)
Prev: 0
hex: 0x01DE
bin: 0000000111011110

Reg16(478)
Prev: 0
hex: 0x01DE
bin: 0000000111011110

如果您需要查看我的完整课程实现,您可以在我的问题下方找到它 - 关注点:我正在寻找对此处行为的清晰和更好的理解。

我想知道的是为什么第一个版本没有产生正确或预期的值,以及为什么第二次尝试会产生。这两种实现之间的主要区别是什么,以及编译器中的内部情况是什么?我正在使用 Visual Studio 2017。


Register.h

#pragma once

#include <algorithm>
#include <bitset>
#include <cassert>
#include <climits>
#include <cstdint>
#include <iterator>
#include <iostream>
#include <iomanip>
#include <limits>
#include <map>
#include <string>
#include <type_traits>

namespace vpc {
    using u8  = std::uint8_t;
    using u16 = std::uint16_t;
    using u32 = std::uint32_t;
    using u64 = std::uint64_t;

    template<typename T>
    struct Register;

    using Reg8  = Register<u8>;
    using Reg16 = Register<u16>;
    using Reg32 = Register<u32>;
    using Reg64 = Register<u64>;

    template<typename T>
    struct Register {
        T value;
        T previous_value;
        std::bitset<sizeof(T)* CHAR_BIT> bits;

        Register() : value{ 0 }, previous_value{ 0 }, bits{ 0 } {}

        template<typename U, std::enable_if_t<(sizeof(U) > sizeof(T))>* = nullptr>
        explicit Register(const U val, const u8 idx = 0) :
            value{ static_cast<T>((val >> std::size(bits) * idx) &
                  std::numeric_limits<std::make_unsigned_t<T>>::max()) },
            previous_value{ 0 },
            bits{ value }
        {
            constexpr u16 sizeT = sizeof(T);
            constexpr u16 sizeU = sizeof(U);
            assert((idx >= 0) && (idx <= ((sizeU / sizeT) - 1)) );
        }

        template<typename U, std::enable_if_t<(sizeof(U) < sizeof(T))>* = nullptr>
        explicit Register(const U val, const u8 idx = 0) :
            value{ static_cast<T>((static_cast<T>(val) << sizeof(U)*CHAR_BIT*idx) &
                  std::numeric_limits<std::make_unsigned_t<T>>::max()) },
            previous_value{ 0 },
            bits{ value }
        {
            constexpr u16 sizeT = sizeof(T);
            constexpr u16 sizeU = sizeof(U);
            assert((idx >= 0) && (idx <= ((sizeT / sizeU) - 1)) );
        }

        template<typename U, std::enable_if_t<(sizeof(U) == sizeof(T))>* = nullptr>
        explicit Register(const U val, const u8 idx = 0) :
            value{ static_cast<T>( val ) }, previous_value{ 0 }, bits{ value }
        {}

        template<typename... Args>
        Register(Args... args) {}

        template<typename U>
        Register(const Register<U>& reg, const u8 idx = 0) : Register(reg.value, idx) {}

        void changeEndian() {
            T tmp = value;
            char* const p = reinterpret_cast<char*>(&tmp);
            for (size_t i = 0; i < sizeof(T) / 2; ++i)
                std::swap(p[i], p[sizeof(T) - i - 1]);
            bits = tmp;
        }

        Register& operator=(const Register& obj) {
            this->value = obj.value;
            this->previous_value = obj.previous_value;
            this->bits = obj.bits;
            return *this;
        }

        template<typename Lhs, typename Rhs>
        friend auto operator+(const Register<Lhs>& l, const Register<Rhs>& r);
    };
} // namespace vpc    

【问题讨论】:

    标签: c++ templates operator-overloading c++17 auto


    【解决方案1】:

    三分。

    (1) 请记住在您的第一个版本的operator+ () 中使用if constexpr,而不是简单的if

    template<typename Lhs, typename Rhs>
    auto operator+(const Register<Lhs>& l, const Register<Rhs>& r) {
        auto tmp = l.value + r.value;
        if constexpr (sizeof(l.value) < sizeof(r.value)) // if constexpr here!
            return Register<Rhs>{ tmp };
        else
            return Register<Lhs>{ tmp };
    }
    

    否则当sizeof(l.value)sizeof(r.value) 不同时,auto 扣除类型不起作用。

    (2) 从operator() 的第一个版本开始(因为你将两个相同类型的值相加而有效),你有一个溢出。

    更准确地说:

    • LhsRhsstd::uint8_t,所以函数返回 Register&lt;std::uint8_t&gt;
    • tmp 变为 std::uint32_t(参见第 3 点),但将其分配给 std::uint8_t 会导致溢出

    (3) 来自我的平台,来自代码

    std::cout << sizeof(char) << std::endl;
    std::cout << sizeof(std::declval<char>()+std::declval<char>()) << std::endl;
    std::cout << sizeof(short) << std::endl;
    std::cout << sizeof(std::declval<short>()+std::declval<short>()) << std::endl;
    

    我明白了

    1
    4
    2
    4
    

    这叫“整体推广”。

    简而言之:两个char之和变成int;两个short 之和成为int

    这应该澄清为什么你的第二版operator+ () 有效(但不是你想要的,我想)

    template<typename Lhs, typename Rhs>
    auto operator+(const Register<Lhs>& l, const Register<Rhs>& r) {
        return Register<decltype(l.value + r.value)>{ l.value + r.value };
    }
    

    你知道decltype(l.value + r.value)int;所以decltype(R8+R8)R32

    【讨论】:

    • 现在我明白了为什么我没有得到0x01DE,因为在第一个版本中被截断了,而在第二个版本中使用decltype,如果可能的话,它会自动提升为更大的类型。谢谢你的详细解释。这是我刚刚忽略的事情。拥有第二双眼睛总是有帮助的!这就是我要求澄清的原因。我怀疑发生了什么事,但无法确定。我确实知道值的截断和整数提升,但可以这么说并没有盯着我看!
    • @FrancisCugler - 糟糕的是我忘记了“整体提升”部分;是我二十多年前就知道的东西(当时我正在用 C 编程)。所以对我也有用。
    • 我已经考虑过,但我认为我更喜欢您的回答,因为您更详细地解释了一些事情,并且您还提供了在第一个版本中使用 constexpr 的建议.
    【解决方案2】:

    什么是RhsLhs什么时候

    r8 + r8
    

    被称为?两者都是uint8_t,所以在第一个版本中

    auto tmp = l.value + r.value;
    if (sizeof(l.value) < sizeof(r.value))
        return Register<Rhs>{ tmp };
    else
        return Register<Lhs>{ tmp };
    

    临时的tmp = 478 被传递给Registervalue 类型为uint8_t,你会丢失数据。


    第二个版本

    return Register<decltype(l.value + r.value)>{ l.value + r.value };
    

    您正在使用decltype 来获取l.value + r.value 的类型。两种类型都是uint_8,但是在进行整数运算时都提升为int,所以decltype()返回intint的宽度足以存储478。

    【讨论】:

    • 感谢您的回答,但您在此处所说的一切都是由另一个人描述的。我不知道谁先回答,所以我给你们两个投了赞成票!正如我之前评论的那样;有时你需要第二双眼睛!第一个版本的价值截断不是直接盯着我看,第二个版本的decltype它会自动进行整数提升。
    • @FrancisCugler - rafix07 是第一个。几分钟,但第一次。
    猜你喜欢
    • 1970-01-01
    • 2020-04-05
    • 2022-07-08
    • 1970-01-01
    • 2015-12-09
    • 2011-05-08
    • 2023-03-11
    • 2015-12-29
    • 1970-01-01
    相关资源
    最近更新 更多