21-11 重载类型转换(typecasts)

在第10.6节——显式类型转换(强制转换)和static_cast中,你了解到C++允许将一种数据类型转换为另一种类型。以下示例展示了将int转换为double的过程:

int n{ 5 };
auto d{ static_cast<double>(n) }; // int cast to a double

C++ 已经知道如何在内置数据类型之间进行转换。然而,默认情况下,C++ 并不知道如何转换我们程序中定义的任何类。

第14.16节——转换构造函数与显式关键字中,我们展示了如何通过转换构造函数将某类型对象转换为另一类型的对象。但此方法仅适用于目标类型为可修改的类类型(即能添加此类构造函数)。若目标类型不符合此条件呢?

请看以下类定义:

class Cents
{
private:
    int m_cents{};
public:
    Cents(int cents=0)
        : m_cents{ cents }
    {
    }

    int getCents() const { return m_cents; }
    void setCents(int cents) { m_cents = cents; }
};

这个类相当简单:它将若干美分存储为整数,并提供访问函数来获取和设置美分数量。它还提供了一个构造函数,用于将整数转换为美分。

既然能将整数转换为美分(通过构造函数),那么我们或许也需要提供将美分转换回整数的方法。在某些情况下这可能并不理想,但在此处确实合理。

作者注:


一首诗:
将整数转为分币
需用构造函数来实现
当然分币转整数也合理
但编译器报错阻拦。
为允许此类转换事件
我们向编译器表达同意
继而定义所有意图下
如何转换这类内容类型。
那么规避编译器静态类型防御的语法是什么?
我们即将详述具体方法
届时你便不再悬着心。

一种不太理想的方法是使用转换函数。在此示例中,我们通过成员函数 getCents() 将 Cents 变量“转换”回 int 类型,以便使用 printInt() 进行打印:

#include <iostream>

void printInt(int value)
{
    std::cout << value;
}

int main()
{
    Cents cents{ 7 };
    printInt(cents.getCents()); // print 7

    std::cout << '\n';

    return 0;
}

image

虽然该函数能产生我们期望的结果,但它并非真正的转换——编译器无法理解应将其用于强制转换或隐式转换。这也意味着,若需频繁执行分币到整数的转换,代码中将充斥着getCents()的调用,显得杂乱无章。

还有其他解决方案吗?


重载类型转换

此时便需要重载类型转换运算符。此类转换既可显式使用(通过显式转换),也可由编译器隐式执行所需的类型转换。

下面演示如何重载类型转换运算符,定义从Cents到int的转换:

class Cents
{
private:
    int m_cents{};
public:
    Cents(int cents=0)
        : m_cents{ cents }
    {
    }

    // Overloaded int cast
    operator int() const { return m_cents; }

    int getCents() const { return m_cents; }
    void setCents(int cents) { m_cents = cents; }
};

为此,我们编写了一个名为 operator int() 的新重载运算符。请注意,在 operator 关键字与目标类型之间存在空格。

这里有几点值得注意:

  • 重载的类型转换必须是非静态成员,且应声明为 const 以便与 const 对象配合使用。
  • 重载的类型转换没有显式参数,因为无法向其传递显式参数。它们仍隐含着指向隐式对象(即待转换对象)的*this参数。
  • 重载类型转换不声明返回类型。转换名称(如int)即作为返回类型,因其是唯一允许的返回类型。此设计避免了声明冗余。

现在在示例中,我们可以这样调用printInt():

#include <iostream>

int main()
{
    Cents cents{ 7 };
    printInt(cents); // print 7

    std::cout << '\n';

    return 0;
}

image

编译器首先会注意到函数 printInt() 具有一个 int 参数。接着它会发现变量 cents 并非 int 类型。最后,它将检查我们是否提供了将 Cents 转换为 int 的方法。由于我们提供了,它将调用我们的 int() 运算符函数,该函数返回一个 int 值,而返回的 int 值将被传递给 printInt()。

此类类型转换也可通过 static_cast 显式调用:

std::cout << static_cast<int>(cents);

你可以为任何数据类型提供重载的类型转换,包括你自己定义的程序数据类型!

下面是一个名为 Dollars 的新类,它提供了重载的 Cents 转换:

class Dollars
{
private:
    int m_dollars{};
public:
    Dollars(int dollars=0)
        : m_dollars{ dollars }
    {
    }

     // Allow us to convert Dollars into Cents
     operator Cents() const { return Cents{ m_dollars * 100 }; }
};

这使我们能够将一个美元对象直接转换为一个分对象!这让你可以这样操作:

#include <iostream>

class Cents
{
private:
    int m_cents{};
public:
    Cents(int cents=0)
        : m_cents{ cents }
    {
    }

    // Overloaded int cast
    operator int() const { return m_cents; }

    int getCents() const { return m_cents; }
    void setCents(int cents) { m_cents = cents; }
};

class Dollars
{
private:
    int m_dollars{};
public:
    Dollars(int dollars=0)
        : m_dollars{ dollars }
    {
    }

    // Allow us to convert Dollars into Cents
    operator Cents() const { return Cents { m_dollars * 100 }; }
};

void printCents(Cents cents)
{
    std::cout << cents; // cents will be implicitly cast to an int here
}

int main()
{
    Dollars dollars{ 9 };
    printCents(dollars); // dollars will be implicitly cast to a Cents here

    std::cout << '\n';

    return 0;
}

因此,该程序将输出以下值:

image

这很合理,毕竟9美元等于900美分!

虽然这证明了这种做法可行,但在这种情况下,为Dollars类添加一个转换构造函数(带有一个类型为Cents的参数)实际上更可取。我们将在下面讨论原因。


显式类型转换

正如我们可以显式声明构造函数以禁止其用于隐式转换,出于相同原因,我们也可以显式声明重载的类型转换。显式类型转换只能通过强制转换(如static_cast)或直接初始化形式(括号或大括号)调用,在执行复制初始化时不会考虑这些转换。

#include <iostream>

class Cents
{
private:
    int m_cents{};
public:
    Cents(int cents=0)
        : m_cents{ cents }
    {
    }

    explicit operator int() const { return m_cents; } // now marked as explicit

    int getCents() const { return m_cents; }
    void setCents(int cents) { m_cents = cents; }
};

class Dollars
{
private:
    int m_dollars{};
public:
    Dollars(int dollars=0)
        : m_dollars{ dollars }
    {
    }

    operator Cents() const { return Cents { m_dollars * 100 }; }
};

void printCents(Cents cents)
{
//  std::cout << cents;                   // no longer works because cents won't implicit convert to an int
    std::cout << static_cast<int>(cents); // we can use an explicit cast instead
}

int main()
{
    Dollars dollars{ 9 };
    printCents(dollars); // implicit conversion from Dollars to Cents okay because its not marked as explicit

    std::cout << '\n';

    return 0;
}

image

类型转换通常应标记为显式。当转换成本较低且转换结果为类似的用户定义类型时,可作例外处理。我们的 Dollars::operator Cents() 类型转换未标记为显式,因为没有理由禁止在预期 Cents 的任何位置使用 Dollars 对象。

最佳实践:
正如单参数转换构造函数应标记为显式转换,类型转换也应标记为显式转换——除非目标类型与转换后类型本质上完全等同。


何时使用转换构造函数与重载类型转换

重载类型转换与转换构造函数功能相似:

  • 转换构造函数converting constructor是类类型B的成员函数,定义了如何从A创建B。
  • 重载类型转换overloaded typecast是类类型A的成员函数,定义如何将A转换为B。

两种方式都以A为起点,最终得到B。主要区别在于转换过程的控制权归属:由A还是B掌控。

由于两种方式都需要定义成员函数,因此仅适用于可修改的类类型。若A不是可修改的类类型,则无法使用重载类型转换;若B不是可修改的类类型,则无法使用转换构造函数。若两者均不可修改,则需改用非成员转换函数。

当A和B均为可修改的类类型时,两种方法皆可适用。但既然只需选择其一,该如何取舍?

通常应优先选择转换构造函数而非重载类型转换。在其他条件相同的情况下,让类类型自主完成构造比依赖其他类进行创建和初始化更为简洁。

最佳实践:
在可能的情况下,优先使用转换构造函数,避免使用重载的类型转换。

以下情况应使用重载的类型转换:

  • 当提供对基本类型的转换时(因为无法为这些类型定义构造函数)。最常见的情况是,在需要将对象用于条件语句时,提供对 bool 的转换。
  • 当转换返回引用或常量引用时。
  • 当提供无法添加成员的类型转换时(例如转换为 std::vector,因该类型同样无法定义构造函数)。
  • 当不希望目标类型知晓转换源类型时。此举有助于避免循环依赖。

关于最后一点的示例:std::string 提供构造函数可从 std::string_view 创建 std::string,这意味着 必须包含 <string_view>。若 std::string_view 提供构造函数可从 std::string 创建自身,则 <string_view> 需包含 ,从而导致头文件间形成循环依赖。

相反,std::string 通过重载类型转换处理从 std::string 到 std::string_view 的转换(这完全合理,因为它本身已包含 <string_view>)。std::string_view 完全不了解 std::string,因此无需包含 。如此便避免了循环依赖。

当同一转换同时定义了转换构造函数和重载类型转换时,重载解析会同时考虑两者。根据重载转换是否为 const、被转换对象是否为 const,以及使用的转换或初始化类型(复制与直接),可能选择任一函数(可能导致类型转换优先于转换构造函数),也可能导致结果歧义(引发编译错误)。因此应避免同时定义可处理相同转换的重载类型转换与转换构造函数。

最佳实践:
当需要定义如何将类型 A 转换为类型 B 时:

  • 若 B 是可修改的类类型,优先使用转换构造函数将 A 转换为 B。
  • 否则,若 A 是可修改的类类型,则使用重载的类型转换将 A 转换为 B。
  • 否则,使用非成员函数将 A 转换为 B。
posted @ 2026-01-25 21:23  游翔  阅读(1)  评论(0)    收藏  举报