четверг, 14 августа 2025 г.

[prog.c++] dynamic_cast и непубличные базовые классы

Всю свою профессиональную жизнь по мере необходимости пользовался dynamic_cast-ом в C++ и не знал, что dynamic_cast не будет делать приведение к ссылке/указателю на непубличный базовый класс.

Вот пример:

#include <iostream>

class iface {
public:
    virtual ~iface() = default;

    virtual void f() = 0;
};

class basic_impl {
public:
    virtual ~basic_impl() = default;

    void impl_f() {}
};

class first_derived final : public iface, protected basic_impl
{
public:
    void f() override { impl_f(); }
};

class second_derived final : public iface, public basic_impl
{
public:
    void f() override { impl_f(); }
};

void try_dynamic_cast(iface * base)
{
    std::cout << "trying to cast " << typeid(*base).name() << std::flush;
    if(auto * p = dynamic_cast<basic_impl *>(base); p)
        std::cout << "... OK" << std::endl;
    else
        std::cout << "... FAILURE" << std::endl;
}

int main() {
    first_derived f;
    second_derived s;

    try_dynamic_cast(&f);
    try_dynamic_cast(&s);
}

Обращаю внимание, что для first_derived класс basic_impl -- это protected-база.

В результате его запуска получим:

trying to cast 13first_derived... FAILURE
trying to cast 14second_derived... OK

Цынк.

PS. Вот так: век живи, век учись. А помрешь все равно дураком 🙂

понедельник, 11 августа 2025 г.

[prog.c++] Оператор <=> и оператор ==

Надеюсь, многие знают, что в C++20 появилась замечательная штука operator<=>. Стоит его определить, как компилятор на его основе выводит другие операторы сравнения, вроде operator< или operator>=.

Причем, что особенно хорошо, можно попросить компилятор сгенерировать operator<=> для нас автоматически:

struct example {
  ... // Какие-то поля.

  auto operator<=>(const example &) const = default;
};

И все. Остальное за нас сделает компилятор.

Однако, думаю, не все знают, что если нам не подходит штатная реализация и мы пишем operator<=> сами, то компилятор потребует, чтобы мы еще и определили свой собственный operator==.

Например, допустим, что у нас в example три поля, а в сравнении должно участвовать только два из них:

struct example {
  int m_a;
  int m_b;
  int m_c;

  auto operator<=>(const example & o) const noexcept
  {
    return std::tie(m_a, m_c) <=> std::tie(o.m_a, o.m_c);
  }
};

В этом случае мы внезапно© обнаружим, что у нас нет operator==.

Таков путь: если мы определили свой operator<=>, то компилятор не будет автоматически выводить равенство и неравенство, а потребует, чтобы мы предоставили operator==.

Может быть вам будет интересно почему?

Если да, то лучше всего прочитать раздел с мотивацией из предложения P1185 и вот этот ответ на Stackoverflow. В двух же словах: реализация сравнения на строгое равенство на базе operator<=> может быть весьма неэффективна.

Например, представим, что мы сравниваем две строки: s1="AA" и s2="AAA".

При лексикографическом сравнении s1 меньше, чем s2: общая подстрока в s1 и s2 совпадает, но в s1 символов меньше.

Когда мы для наших строк s1 и s2 вызываем operator<=>, то этот оператор пытается выяснить как именно соотносятся s1 и s2: меньше ли s1, чем s2, или же s1 больше, чем s2, или же они равны.

Теперь допустим, что в s1 находится миллион символов "A", а в s2 -- миллион и один символ "A". При вызове operator<=> придется сравнить миллион символов в общей подстроке прежде чем мы сможем понять, что s1 все-таки меньше, т.к. у нее символов меньше.

При этом мы не можем прервать сравнение раньше. Допустим, что в s1 будет девятьсот девяносто девять тысяч и девятьсот девяносто девять символов "A", а за ними единственный символ "B", тогда как в s2 все миллион + один символ -- это "A". В таком случае строка s1 окажется больше, не смотря на то, что символов в ней меньше.

Однако, если нам не нужно выяснять общее соотношение между s1 и s2, а достачно просто понять равны ли они или нет, то мы можем начать со сравения длин и лишь затем переходить к посимвольному сравнению.

Когда компилятор по нашей просьбе сам генерирует operator<=>, то он эти фокусы учитывает (под катом небольшой тестовый бенчмарк, который позволяет убедится, что сравнение двух неравных векторов требует минимального времени). Однако, если operator<=> предоставляет пользователь, то у компилятора нет уверенности в том, что пользователь написал свой operator<=> с учетом возможностей оптимизации операции "строгое равенство". И просит нас взять ответственность за operator== на себя.