Как работают операторные функции, если они определены в терминах класса, а не фактического оператора? - PullRequest
0 голосов

Я изучал онлайн-курс по шаблонам проектирования в C ++ и наткнулся на странное «приведение» (?) С использованием объявления функции operator.

Минимальная настройка следующая (фактический код ниже):

class A {
    ...

    static B build();
};

class B {
    A a;
};


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

Поскольку функция build возвращает объект типа B, существует несоответствие типов, и код не может быть скомпилирован. Чтобы исправить это, инструктор определил следующую функцию в классе B:

operator A() { return a; }

Мой вопрос: как это работает? Я понимаю механизм перегрузки операторов, но в этом случае мы перегружаем реальный класс, а не оператор. Что делается, когда мы объявляем операторную функцию, используя другой класс? Более того, тип возвращаемого значения не определен. Предполагает ли компилятор, что возвращаемый тип совпадает с классом, в котором определена функция? (т.е. ... B operator A() { ... }) Я не могу по-настоящему обдумать эту концепцию, интуитивно.

Я никогда не слышал об этом методе, не говоря уже о том, что он вообще возможен, прежде чем сталкивался с ним только сейчас. Я пытался исследовать это в Интернете, но, по понятным причинам, я бы сказал, все мои результаты поиска возвращают ссылки для базовой перегрузки или, по крайней мере, более традиционной перегрузки с использованием операторов.

<ч />

Для контекста, эта лекция посвящена шаблону проектирования «Построитель» с использованием элемента 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>

При демонстрации паттерна «беглый строитель» инструктор заставил нас изменить нашу функцию addChild, чтобы она возвращала ссылку на структуру строителя.

Функция HtmlBuilder::addChild изменяется следующим образом: тип возвращаемого значения изменяется с void на HtmlBuilder& (возвращая *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>

Успешно определив и применив шаблон беглого строителя, инструктор поставил следующий вопрос:

Как мы можем получить объект элемента Html из нашей build функции?

Моей непосредственной реакцией было подумать о том, чтобы, возможно, предоставить метод получения для класса HtmlBuilder. Что-то тривиальное, как это:

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

Затем вы бы «собрали и получили» элемент следующим образом:

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';
}

Тогда оба выхода будут одинаковыми. Однако инструктор выбрал совершенно другой и гораздо более интересный метод. Вместо того, чтобы сделать это в два этапа, используя мой подход «построить и получить», он сделал следующее.

Сначала он переписал функцию main следующим образом (обратите внимание, что он в отличие от меня строит и получает элемент за один шаг):

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

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

Первоначально компилятор отклоняет эту модификацию, потому что результатом вызова HtmlElement::build является объект HtmlBuilder. Итак, чтобы решить эту проблему, вторым, что сделал инструктор, было определение следующей функции в классе HtmlBuilder:

operator HtmlElement() const { return root; }

Сделав это, код компилируется без помех и вывод приложения снова:

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

Опять же, мой вопрос: почему или как это работает? Что делается, когда мы объявляем функцию operator, используя другой класс? Я понимаю махинации обычного оператора перегрузки. Для меня имеет смысл перегрузить (), [] или =, но я не понимаю, как и почему этот случай работает. Нет даже объявленного типа возврата; компилятор просто предполагает, что он должен возвращать текущий тип класса?

Спасибо, что уделили время всем.

Ответы [ 2 ]

0 голосов
/ 01 мая 2018

Re: «мы перегружаем реальный класс». № operator A() { return a; } перегружает оператора; обратите внимание на ключевое слово operator. Это определяет оператор преобразования , который будет использоваться, когда код требует преобразования из объекта типа B в объект типа A.

Использование в вашем примере немного неясно. Вот более простой пример:

B b;
A obj = b;

Создание объекта obj требует преобразования объекта b в объект типа A, и эту работу выполняет operator A().

В вашем примере вызов A::build возвращает объект типа B, поэтому в коде

A obj = A::build();

вызов A::build() возвращает временный объект типа B, и оператор преобразования (operator A()) преобразует этот объект в объект типа A, который используется для инициализации obj.

0 голосов
/ 01 мая 2018

Это определяемое пользователем преобразование . Тип возвращаемого значения - это тип после operator, то есть тип назначения. Это добавляет дополнительное неявное преобразование для типа, которое используется всякий раз, когда рассматривается неявное преобразование.

Использование слова operator на самом деле не совсем ясное ключевое слово, потому что оно не действительно определяет оператор (хотя он может взаимодействовать с операциями приведения), но я думаю, что это было избегайте добавления другого зарезервированного слова.

...