当 'operator' 函数是根据 class 而不是实际运算符定义时,它们如何工作?

How do 'operator' functions work when they're defined in terms of a class rather than an actual operator?

我正在探索有关 C++ 设计模式的在线课程,我遇到了一个奇怪的 "cast" (?) 使用 operator 函数声明。

最小设置如下(实际代码如下):

class A {
    ...

    static B build();
};

class B {
    A a;
};


int main()
{
    A obj = A::build();
}

由于 build 函数 return 是类型 B 的对象,因此存在类型不匹配,代码无法编译。为了纠正这个问题,教师在 class B:

中定义了以下函数
operator A() { return a; }

我的问题是,这是如何工作的?我了解重载运算符的机制,但在这种情况下,我们重载的是实际的 class,而不是运算符。当我们使用另一个 class 声明一个运算符函数时发生了什么?而且,没有定义 return 类型,编译器是否只是假设 return 类型与定义函数的 class 类型相同? (即... B operator A() { ... })直觉上我无法真正理解这个概念。

我根本没听说过这种方法,更别提在刚才遇到之前认为它是可能的了。我一直在尝试在线研究这个问题,但是 - 可以理解,我会说 - 我所有的搜索结果 return 基本重载链接,或者至少是更传统的重载,使用运算符。


就上下文而言,本讲座是关于 "Builder" 设计模式,使用 Html 元素和 Html 构建器结构。这是我的基本代码,还没有修改。

#include <iostream>
#include <sstream>
#include <string>
#include <vector>

struct HtmlBuilder;

struct HtmlElement {
    std::string name;
    std::string text;

    std::vector<HtmlElement> elements;

    const std::size_t indent_size = 2;

    std::string str(const int indent = 0) const {
        std::ostringstream oss;

        std::string indentation(indent_size * indent, ' ');

        oss << indentation << "<" << name << ">\n";

        if (!text.empty())
            oss << std::string(indent_size * (indent + 1), ' ') << text << '\n';

        for (const auto& element : elements)
            oss << element.str(indent + 1);

        oss << indentation << "</" << name << ">\n";

        return oss.str();
    }

    static HtmlBuilder build(const std::string& rootName);
};

struct HtmlBuilder {
    HtmlElement root;

    void addChild(const std::string& childName, const std::string& childText) {
        HtmlElement childElement { childName, childText };
        root.elements.emplace_back(childElement);
    }

    std::string str() const { return root.str(); }
};

HtmlBuilder HtmlElement::build(const std::string& rootName) {
    return { rootName };
}

int main()
{
    HtmlBuilder builder { "ul" };
    builder.addChild("li", "hello");
    builder.addChild("li", "world");

    std::cout << builder.str();
}

输出,如预期:

<ul>
  <li>
    hello
  </li>
  <li>
    world
  </li>
</ul>

在演示 "fluent builder" 模式时,讲师让我们将 addChild 函数修改为 return 对构建器结构的引用。

HtmlBuilder::addChild函数修改如下:return类型由void改为HtmlBuilder&(returning *this )

HtmlBuilder& addChild(const std::string& childName, const std::string& childText) {
    HtmlElement childElement { childName, childText };
    root.elements.emplace_back(childElement);

    return *this;
}

然后重写main函数:

int main()
{
    auto builder = HtmlElement::build("ul").addChild("li", "hello").addChild("li", "world");

    std::cout << builder.str();
}

输出又是:

<ul>
  <li>
    hello
  </li>
  <li>
    world
  </li>
</ul>

成功定义并实施了流畅的构建器模式后,讲师现在提出以下问题:

How could we get an Html element object from our build function?

我的第一反应是考虑为 HtmlBuilder class 提供一个 getter 方法。一些琐碎的事情,像这样:

struct HtmlBuilder {
    ...
    HtmlElement getElement() const { return root; }
};

然后您将 "build and get" 元素像这样:

int main()
{
    const auto builder = HtmlElement::build("ul").addChild("li", "hello").addChild("li", "world");
    const auto element = builder.getElement();

    std::cout << builder.str() << '\n';
    std::cout << element.str() << '\n';
}

两个输出将是相同的。然而,导师选择了一种截然不同且有趣得多的方法。他没有通过我的 "build and get" 方法分两步完成,而是执行了以下操作。

他首先像这样重写了 main 函数(请注意,他一步构建和获取元素,这与我不同):

int main()
{
    HtmlElement element = HtmlElement::build("ul").addChild("li", "hello").addChild("li", "world");

    std::cout << element.str();
}

最初编译器拒绝此修改,因为 HtmlElement::build 调用的结果是一个 HtmlBuilder 对象。所以为了解决这个问题,导师做的第二件事就是在HtmlBuilderclass中定义如下函数:

operator HtmlElement() const { return root; }

完成后,代码编译顺利,应用程序输出再次为:

<ul>
  <li>
    hello
  </li>
  <li>
    world
  </li>
</ul>

同样,我的问题是,为什么或如何工作?当我们使用另一个 class 声明一个 operator 函数时发生了什么?我了解通常的运算符重载的阴谋。重载 ()[]= 对我来说很直观,但我不明白这种情况如何或为何起作用。甚至没有声明的 return 类型;编译器是否只是假设它意味着 return 当前 class 类型?

谢谢大家的宝贵时间。

那是 user-defined conversion。 return 类型是 operator 之后的类型,即目标类型。这为类型添加了一个额外的隐式转换,每当考虑隐式转换时都会使用它。

使用 operator 这个词确实不是最清晰的关键字,因为它 真的 没有定义运算符(尽管它可以与强制转换操作交互), 但我想这是为了避免添加另一个保留字。

回复:"we're overloading an actual class"。不,operator A() { return a; } 正在重载运算符;注意关键字 operator。这定义了一个 转换运算符 ,当代码调用从类型 B 的对象到类型 A.[=27 的对象的转换时,将使用该运算符=]

您的示例中的用法有点晦涩。这是一个更简单的例子:

B b;
A obj = b;

创建 obj 对象需要将 b 对象转换为类型 A 的对象,这就是 operator A() 所做的工作。

在您的示例中,调用 A::build returns 类型为 B 的对象,因此在代码中

A obj = A::build();

调用 A::build() returns 类型为 B 的临时对象,转换运算符 (operator A()) 将该对象转换为类型为 A,用来初始化obj.