Как правильно использовать основные рекомендации C++: C.146: используйте dynamic_cast, когда навигация по иерархии классов неизбежна.

Мотивация

Основные рекомендации C++ рекомендуют использовать dynamic_cast, когда навигация по иерархии классов неизбежна. Это приводит к тому, что clang-tidy выдает следующую ошибку: Do not use static_cast to downcast from a base to a derived class; use dynamic_cast instead [cppcoreguidelines-pro-type-static-cast-downcast].

Далее в руководстве говорится:

Примечание.

Как и другие приведения, dynamic_cast используется слишком часто. Предпочитайте virtual функции кастингу. Предпочитайте статический полиморфизм навигации по иерархии там, где это возможно (не требуется разрешение во время выполнения) и достаточно удобно.

Я всегда просто использовал enum с именем Kind, вложенным в мой базовый класс, и выполнял static_cast на основе его типа. Чтение основных рекомендаций C++, ... Тем не менее, по нашему опыту, такие ситуации, как я знаю, что делаю, по-прежнему являются известным источником ошибок. предполагает, что я не должен этого делать. Часто у меня нет никаких функций virtual, поэтому RTTI не используется для использования dynamic_cast (например, я получу error: 'Base_discr' is not polymorphic). Я всегда могу добавить функцию virtual, но это звучит глупо. В руководстве также говорится о проведении сравнительного анализа, прежде чем рассматривать возможность использования дискриминантного подхода, который я использую с Kind.

Эталонный показатель


enum class Kind : unsigned char {
    A,
    B,
};


class Base_virt {
public:
    Base_virt(Kind p_kind) noexcept : m_kind{p_kind}, m_x{} {}

    [[nodiscard]] inline Kind
    get_kind() const noexcept {
        return m_kind;
    }

    [[nodiscard]] inline int
    get_x() const noexcept {
        return m_x;
    }

    [[nodiscard]] virtual inline int get_y() const noexcept = 0;

private:
    Kind const m_kind;
    int m_x;
};


class A_virt final : public Base_virt {
public:
    A_virt() noexcept : Base_virt{Kind::A}, m_y{} {}

    [[nodiscard]] inline int
    get_y() const noexcept final {
        return m_y;
    }

private:
    int m_y;
};


class B_virt : public Base_virt {
  public:
    B_virt() noexcept : Base_virt{Kind::B}, m_y{} {}

  private:
    int m_y;
};


static void
virt_static_cast(benchmark::State& p_state) noexcept {
    auto const a = A_virt();
    Base_virt const* ptr = &a;

    for (auto _ : p_state) {
        benchmark::DoNotOptimize(static_cast<A_virt const*>(ptr)->get_y());
    }
}
BENCHMARK(virt_static_cast);


static void
virt_static_cast_check(benchmark::State& p_state) noexcept {
    auto const a = A_virt();
    Base_virt const* ptr = &a;

    for (auto _ : p_state) {
    if (ptr->get_kind() == Kind::A) {
        benchmark::DoNotOptimize(static_cast<A_virt const*>(ptr)->get_y());
        } else {
            int temp = 0;
        }       
    }
}
BENCHMARK(virt_static_cast_check);


static void
virt_dynamic_cast_ref(benchmark::State& p_state) {
    auto const a = A_virt();
    Base_virt const& reff = a;

    for (auto _ : p_state) {
        benchmark::DoNotOptimize(dynamic_cast<A_virt const&>(reff).get_y());
    }
}
BENCHMARK(virt_dynamic_cast_ref);


static void
virt_dynamic_cast_ptr(benchmark::State& p_state) noexcept {
    auto const a = A_virt();
    Base_virt const& reff = a;

    for (auto _ : p_state) {
        benchmark::DoNotOptimize(dynamic_cast<A_virt const*>(&reff)->get_y());
    }
}
BENCHMARK(virt_dynamic_cast_ptr);


static void
virt_dynamic_cast_ptr_check(benchmark::State& p_state) noexcept {
    auto const a = A_virt();
    Base_virt const& reff = a;

    for (auto _ : p_state) {
        if (auto ptr = dynamic_cast<A_virt const*>(&reff)) {
            benchmark::DoNotOptimize(ptr->get_y());
        } else {
            int temp = 0;
        }
    }
}
BENCHMARK(virt_dynamic_cast_ptr_check);


class Base_discr {
public:
    Base_discr(Kind p_kind) noexcept : m_kind{p_kind}, m_x{} {}

    [[nodiscard]] inline Kind
    get_kind() const noexcept {
        return m_kind;
    }

    [[nodiscard]] inline int
    get_x() const noexcept {
        return m_x;
    }

private:
    Kind const m_kind;
    int m_x;
};


class A_discr final : public Base_discr {
public:
    A_discr() noexcept : Base_discr{Kind::A}, m_y{} {}

    [[nodiscard]] inline int
    get_y() const noexcept {
        return m_y;
    }

private:
    int m_y;
};


class B_discr : public Base_discr {
public:
    B_discr() noexcept : Base_discr{Kind::B}, m_y{} {}

private:
    int m_y;
};


static void
discr_static_cast(benchmark::State& p_state) noexcept {
    auto const a = A_discr();
    Base_discr const* ptr = &a;

    for (auto _ : p_state) {
        benchmark::DoNotOptimize(static_cast<A_discr const*>(ptr)->get_y());
    }
}
BENCHMARK(discr_static_cast);


static void
discr_static_cast_check(benchmark::State& p_state) noexcept {
    auto const a = A_discr();
    Base_discr const* ptr = &a;

    for (auto _ : p_state) {
        if (ptr->get_kind() == Kind::A) {
            benchmark::DoNotOptimize(static_cast<A_discr const*>(ptr)->get_y());
        } else {
            int temp = 0;
        }
    }
}
BENCHMARK(discr_static_cast_check);

Я новичок в бенчмаркинге, поэтому я действительно не знаю, что делаю. Я позаботился о том, чтобы virtual и дискриминантные версии имели одинаковую структуру памяти, и изо всех сил старался предотвратить оптимизацию. Я выбрал уровень оптимизации O1, так как все, что выше, не представлялось репрезентативным. discr означает распознанный или помеченный. virt означает virtual Вот мои результаты:

Результаты сравнения

Вопросы

Итак, мои вопросы: как мне преобразовать базовый тип в производный, когда (1) я знаю производный тип, потому что я проверил его перед входом в функцию и (2) когда я еще не знаю производный тип. Кроме того, (3) Должен ли я вообще беспокоиться об этом руководстве или мне следует отключить предупреждение? Здесь важна производительность, но иногда это не так. Что я должен использовать?

РЕДАКТИРОВАТЬ:

Использование dynamic_cast кажется правильным ответом на понижение рейтинга. Тем не менее, вам все равно нужно знать, к чему вы понижаетесь, и иметь функцию virtual. Во многих случаях вы не знаете без разбора, такого как kind или tag, что такое производный класс. (4) В случае, когда мне уже нужно проверить, на какой kind объекта я смотрю, следует ли мне по-прежнему использовать dynamic_cast? Разве это не проверка одного и того же дважды? (5) Есть ли разумный способ сделать это без tag?

Пример

Рассмотрим иерархию class:

class Expr {
public:
    enum class Kind : unsigned char {
        Int_lit_expr,
        Neg_expr,
        Add_expr,
        Sub_expr,
    };

    [[nodiscard]] Kind
    get_kind() const noexcept {
        return m_kind;
    }

    [[nodiscard]] bool
    is_unary() const noexcept {
        switch(get_kind()) {
            case Kind::Int_lit_expr:
            case Kind::Neg_expr:
                return true;
            default:
                return false;
        }
    }

    [[nodiscard]] bool
    is_binary() const noexcept {
        switch(get_kind()) {
            case Kind::Add_expr:
            case Kind::Sub_expr:
                return true;
            default:
                return false;
        }
    }

protected:
    explicit Expr(Kind p_kind) noexcept : m_kind{p_kind} {}

private:
    Kind const m_kind;
};


class Unary_expr : public Expr {
public:
    [[nodiscard]] Expr const*
    get_expr() const noexcept {
        return m_expr;
    }

protected:
    Unary_expr(Kind p_kind, Expr const* p_expr) noexcept :
        Expr{p_kind},
        m_expr{p_expr} {}

private:
    Expr const* const m_expr;
};


class Binary_expr : public Expr {
public:
    [[nodiscard]] Expr const*
    get_lhs() const noexcept {
        return m_lhs;
    }

    [[nodiscard]] Expr const*
    get_rhs() const noexcept {
        return m_rhs;
    }

protected:
    Binary_expr(Kind p_kind, Expr const* p_lhs, Expr const* p_rhs) noexcept :
        Expr{p_kind},
        m_lhs{p_lhs},
        m_rhs{p_rhs} {}

private:
    Expr const* const m_lhs;
    Expr const* const m_rhs;
};


class Add_expr : public Binary_expr {
public:
    Add_expr(Expr const* p_lhs, Expr const* p_rhs) noexcept : 
        Binary_expr{Kind::Add_expr, p_lhs, p_rhs} {}
};

Сейчас в main():

int main() {
    auto const add = Add_expr{nullptr, nullptr};
    Expr const* const expr_ptr = &add;

    if (expr_ptr->is_unary()) {
        auto const* const expr = static_cast<Unary_expr const* const>(expr_ptr)->get_expr();
    } else if (expr_ptr->is_binary()) {
        // Here I use a static down cast after checking it is valid
        auto const* const lhs = static_cast<Binary_expr const* const>(expr_ptr)->get_lhs();
    
        // error: cannot 'dynamic_cast' 'expr_ptr' (of type 'const class Expr* const') to type 'const class Binary_expr* const' (source type is not polymorphic)
        // auto const* const rhs = dynamic_cast<Binary_expr const* const>(expr_ptr)->get_lhs();
    }
}
<source>:99:34: warning: do not use static_cast to downcast from a base to a derived class [cppcoreguidelines-pro-type-static-cast-downcast]

        auto const* const expr = static_cast<Unary_expr const* const>(expr_ptr)->get_expr();

                                 ^

Не всегда мне нужно приводить к Add_expr. Например, у меня может быть функция, которая выводит любые Binary_expr. Нужно только привести его к Binary_expr, чтобы получить lhs и rhs. Чтобы получить символ оператора (например, «-» или «+» ...), он может включить kind. Я не вижу, как dynamic_cast поможет мне здесь, и у меня также нет виртуальных функций, на которых можно использовать dynamic_cast.

РЕДАКТИРОВАТЬ 2:

Я отправил ответ, сделав get_kind() virtual, в целом это кажется хорошим решением. Однако теперь я ношу около 8 байтов для vtbl_ptr вместо байта для тега. Объект, созданный из classes, производного от Expr, будет намного превосходить любые другие типы объектов. (6) Это хорошее время, чтобы пропустить vtbl_ptr или я должен предпочесть безопасность dynamic_cast?


person user1032677    schedule 21.08.2020    source источник
comment
по первому вопросу: если вы проверили перед функцией и знаете, что она производного типа, то почему функция берет ссылку на базу?   -  person 463035818_is_not_a_number    schedule 21.08.2020
comment
Я новичок в бенчмаркинге, поэтому я действительно не знаю, что делаю. Я ... изо всех сил старался предотвратить оптимизацию. C++ без оптимизаций используется для отладки кода, но результирующая производительность бессмысленна. Как правило, это не большая проблема, так как отладка вручную является самым медленным компонентом.   -  person MSalters    schedule 21.08.2020
comment
Если у вас нет virtual и вы static_cast от родителя к ребенку, я бы подозрительно. Похоже, композиция была бы лучше, и актерский состав также можно было бы избежать. Если вы знаете тип перед входом в функцию, приведите его перед входом в функцию. если вы еще не знаете производный тип, тогда RTTI (с виртуальным...)/dynamic_cast является решением, вы также можете использовать static_cast, так как он быстрее. Вот где вы должны провести линию.   -  person Matthieu Brucher    schedule 21.08.2020
comment
@MatthieuBrucher, поэтому я выполнял приведение типов перед входом в функцию, но это просто перемещает проблему к тому, какое приведение использовать перед входом в функцию.   -  person user1032677    schedule 21.08.2020
comment
@MSalters, если я использую более высокую оптимизацию, программа оптимизируется. Компилятору просто нужно слишком много информации для этого примера. Что, если я выполняю приведение от Expr к Binary_expr или Add_expr. Нет virtual функций в иерархии.   -  person user1032677    schedule 21.08.2020
comment
@ user1032677: Итак, вы обнаружили, что в реальных условиях операции могут занимать 0,0 секунды. Это показывает, насколько хорошо оптимизатор понимает, что делает dynamic_cast.   -  person MSalters    schedule 21.08.2020
comment
@MSalters Я имею в виду, что объект был объявлен в предыдущей строке. Он не сделает эту оптимизацию, если он пришел из бесплатного магазина, где он не может знать, какая функция создала объект, что более вероятно в моем случае.   -  person user1032677    schedule 21.08.2020
comment
Ну, тогда сравните то, что вы пытаетесь сравнить. Эта конкретная оптимизация реальна и полезна. Для пользователей, которые следуют руководству, с которого начался вопрос, это означает, что у них нет штрафа во время выполнения, когда компилятор может его избежать.   -  person MSalters    schedule 21.08.2020
comment
@MSalters Вы были правы: youtu.be/ARYP83yNAWk?t=206   -  person user1032677    schedule 21.08.2020
comment
@MatthieuBrucher @MSalters Я отредактировал вопрос и рассказал, что я действительно пытаюсь сделать. У меня нет virtual функций, но я хочу понизить уровень. Какие-нибудь мысли?   -  person user1032677    schedule 22.08.2020
comment
Я считаю, что если у вас нет virtual, но вы хотите сделать понижение, то у вас проблемы с дизайном. Возможно, есть сложная причина, по которой вы хотите это сделать, но если вам приходится задавать этот вопрос, это означает, что у вас нет необходимых навыков для этого, и поэтому вам не следует использовать этот дизайн. Состав кажется более адекватным.   -  person Matthieu Brucher    schedule 22.08.2020
comment
@Matthieu Brucher, как насчет моего примера Expr, где vtbl_ptr занимает 50% макета памяти объектов для Unary_expr? Классы, производные от Expr (или аналогичные, такие как Type), будут наиболее инстанцируемыми объектами. CRTP, похоже, решает эту проблему, но мне все равно нужно будет использовать указатель на базовый случай. Ответ @ xryl669 предлагает использовать функцию virtual, чтобы пометить его, но это как бы портит суть (или я что-то упустил?). Поскольку все объекты, которые я выделяю, будут занимать всего несколько байтов, я не уверен, что vptr является хорошим компромиссом, даже если он быстрый.   -  person user1032677    schedule 22.08.2020
comment
@user1032677: А как насчет моего примера с Expr, где vtbl_ptr занимает 50% макета памяти объектов для Unary_expr? Почему Expr является базовым классом, а не какой-то формой вариантного типа, где возможности четко указано? Вероятно, это то, о чем Матье говорил с проблемой дизайна: есть лучшие способы сделать то, что вы пытаетесь сделать, чем использование наследования. Ваш класс Expr вполне может быть std::variant<BinaryOp, Literal>, где BinaryOp — это структура, содержащая два других подвыражения и значение, указывающее, какую операцию она выполняет.   -  person Nicol Bolas    schedule 22.08.2020
comment
@NicolBolas, я понимаю, что ты говоришь, и это очень полезно. Однако в моем случае у меня около 40 разных kind. gtoe_expr, modulo_assignment_expr, double_lit_expr. Я действительно должен упаковать все это в std::variant?   -  person user1032677    schedule 23.08.2020
comment
@user1032677: есть причина, по которой я проигнорировал несколько ваших типов и разбил их на два: бинарный оператор (который представляет любой бинарный оператор, с конкретным экземпляром, хранящим конкретный тип оператора) и литерал терминальное значение. Вам не нужен отдельный тип для каждого вида бинарного оператора. Вы злоупотребляете ООП, что обычно и происходит, когда вы начинаете хотеть вот так опускаться.   -  person Nicol Bolas    schedule 23.08.2020
comment
@NicolBolas, поэтому я все равно буду использовать тег (один enum class для Unary_expr, другой для Binary_expr, ...). Это приемлемый ответ. Спасибо!   -  person user1032677    schedule 23.08.2020
comment
@NicolBolas Да, действительно, спасибо за лучшее объяснение! @user#032677 user#032677 Я думаю, вам нужно еще немного попрактиковаться, сделать несколько шагов назад, поэкспериментировать с другими проблемами, прежде чем вернуться к этой, и должным образом улучшить дизайн.   -  person Matthieu Brucher    schedule 23.08.2020


Ответы (4)


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

Чрезмерное использование ООП — один из примеров такой вещи. Давайте возьмем ваш пример Expr, который является узлом в дереве выражений. И вы можете задавать ему вопросы, например, является ли это бинарной операцией, унарной операцией или нулевой операцией (к вашему сведению: литеральные значения являются нулевыми, а не унарными. Они не принимают аргументов).

Где вы злоупотребляли ООП, так это в попытке дать каждому оператору свой собственный тип класса. Чем отличается оператор сложения от оператора умножения? Приоритет? Это вопрос грамматики; это не имеет значения, если вы построили дерево выражений. Единственная операция, которая действительно заботится о конкретном бинарном операторе, — это когда вы его вычисляете. И даже при выполнении оценки единственная особенная часть — это когда вы берете результаты оценки операндов и вводите их в код, который будет производить результат этой операции. Все остальное одинаково для всех бинарных операций.

Итак, у вас есть одна функция, которая отличается для разных бинарных операций. Если изменяется только одна функция, вам действительно не нужны разные типы только для этого. Гораздо разумнее, чтобы разные бинарные операторы были разными значениями в пределах общего класса BinaryOp. То же самое касается UnaryOp и NullaryOps.

Таким образом, в этом примере есть только 3 возможных типа для любого заданного узла. И это очень разумно иметь дело с variant<NullaryOp, UnaryOp, BinaryOp>. Таким образом, Expr может просто содержать один из них, причем каждый тип операнда имеет ноль или более указателей на его дочерние элементы Expr. В Expr может быть общий интерфейс для получения количества дочерних элементов, перебора дочерних элементов и т. д. А различные типы Op могут предоставлять реализации для них с помощью простых посетителей.

Большинство случаев, когда вы начинаете хотеть делать даункастинг и подобные вещи, — это случаи, которые можно решить лучше и чище, используя другие механизмы. Если вы строите иерархии без функций virtual, где код, получающий базовые классы, уже знает большинство или все возможные производные классы, велика вероятность, что вы действительно пишете грубую форму variant.

person Nicol Bolas    schedule 23.08.2020
comment
Итак, в самой простой форме Expr* указывает на std::variant? - person user1032677; 24.08.2020
comment
Потенциальный недостаток дизайна этого метода заключается в том, что вы повторяетесь. variant хранит информацию о арности выражения, но для каждого типа семейства выражений потребуется тег kind, чтобы сообщить вам, смотрите ли вы на выражение сложения или вычитания. Если мой тег kind говорит, что это addition, то я знаю, что это Binary_expr. Разве это не противоречит принципу DRY и не использует больше памяти, чем необходимо? - person user1032677; 24.08.2020
comment
@ user1032677: Я не понимаю, как это связано с DRY. Вопросы, является ли это бинарной операцией и что это за бинарная операция, — это два отдельных вопроса, задаваемые двумя отдельными местами в коде, которые имеют два разных ответа, что приводит к двум разным использованиям. Вопрос о потреблении памяти слишком тривиален, чтобы его рассматривать; даже если у вас есть дерево выражений со 100 000 узлов, вы говорите о менее чем 512 КБ дополнительной памяти. - person Nicol Bolas; 24.08.2020
comment
Не наследуя от Expr, не злоупотребляю ли я отношением Is-A? - person user1032677; 28.08.2020

Вас может заинтересовать Curious Recursing Template Pattern здесь, чтобы вообще избежать необходимости в виртуальном методе, если вы знаете тип экземпляра во время компиляции.

template <typename Impl> 
class Base_virt {
public:
    Base_virt(Kind p_kind) noexcept : m_kind{p_kind}, m_x{} {}

    [[nodiscard]] inline Kind
    get_kind() const noexcept { return Impl::kind(); }

    [[nodiscard]] inline int
    get_x() const noexcept {
        return m_x;
    }

    [[nodiscard]] inline int get_y() const noexcept { 
        return static_cast<const Impl*>(this)->get_y(); 
    }

private:
    int m_x;
};


class A_virt final : public Base_virt<A_virt> {
public:
    A_virt() noexcept : Base_virt{Kind::A}, m_y{} {}

    [[nodiscard]] inline static Kind kind() { return Kind::A; }

    [[nodiscard]] inline int
    get_y() const noexcept final {
        return m_y;
    }

private:
    int m_y;
};

// Copy/paste/rename for B_virt

В этом случае в dynamic_cast вообще нет необходимости, так как все известно во время компиляции. Вы теряете возможность сохранить указатель на Base_virt (если только вы не создадите базовый класс BaseTag, из которого наследуется Base_virt). Код для вызова такого метода должен быть шаблоном:

template <typename Impl>
static void
crtp_cast_check(benchmark::State& p_state) noexcept {
    auto const a = A_virt();
    Base_virt<Impl> const* ptr = &a;

    for (auto _ : p_state) {
        benchmark::DoNotOptimize(ptr->get_y());
    }
}
BENCHMARK(crtp_static_cast_check<A_virt>);

Это, вероятно, скомпилировано в прямой вызов for(auto _ : p_state) b::dno(m_y). Недостатком этого подхода является раздутое двоичное пространство (у вас будет столько экземпляров функции, сколько дочерних типов), но это будет самым быстрым, поскольку компилятор выведет тип во время компиляции.

С подходом BaseTag это будет выглядеть так:

   class BaseTag { virtual Kind get_kind() const = 0; }; 
   // No virtual destructor here, since you aren't supposed to manipulate instance via this type

   template <typename Impl>
   class Base_virt : BaseTag { ... same as previous definition ... };

   // Benchmark method become
   void virt_bench(BaseTag & base) {
     // This is the only penalty with a virtual method:
     switch(base.get_kind()) {

       case Kind::A : static_cast<A_virt&>(base).get_y(); break;
       case Kind::B : static_cast<B_virt&>(base).get_y(); break;
       ...etc...
       default: assert(false); break; // At least you'll get a runtime error if you forget to update this table for new Kind
     }
     // In that case, there is 0 advantage not to make get_y() virtual, but
     // if you have plenty of "pseudo-virtual" method, it'll become more 
     // interesting to consult the virtual table only once for get_kind 
     // instead of for each method
   }

   template <typename Class>
   void static_bench(Class & inst) {
     // Lame code:
     inst.get_y();
   }

   A_virt a;
   B_virt b;

   virt_bench(a);
   virt_bench(b);

   // vs
   static_bench(a);
   static_bench(b);

Извините за псевдокод выше, но вы поняли.

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

person xryl669    schedule 21.08.2020
comment
Подход BaseTag кажется эквивалентным моему подходу Kind, за исключением того, что get_kind() теперь virtual. Узнав об нисходящей оптимизации dynamic_cast, может показаться, что она всегда будет так же хороша, как проверка tag, а затем static_cast на основе этого. - person user1032677; 21.08.2020
comment
Да, но если вы связываете свой код с последовательностью dynamic_cast (if dc<A> else if dc<B> else if dc<C> etc..), то вы заплатите за виртуальный тест X раз (поскольку вы не можете включить тип в C++), а с приложение BaseTag & Kind, вы можете. - person xryl669; 24.08.2020

Возможное решение — сделать get_kind() функцией virtual. Затем вы можете использовать dynamic_cast. Если вы собираетесь вызывать много функций virtual, вы можете понизить его до наиболее производного класса, чтобы оптимизатор мог оптимизировать вызовы virtual. Вы также захотите использовать наследование virtual (например, class Unary_expr : public virtual Expr {};, если у вас нет членов данных в базовом классе для правильного использования памяти. Наличие указателя на vtable занимает 8 байтов на 64-битной машине, поэтому вы можете быть вынуждены использовать дискриминацию для уменьшения размера каждого объекта (но это, очевидно, имеет смысл только в том случае, если не будут использоваться абсолютно никакие функции virtual).

Этот метод решает следующую проблему, поднятую в руководящих принципах:

... Тем не менее, по нашему опыту, такие ситуации, как я знаю, что делаю, по-прежнему являются известным источником ошибок.

@ xryl669 указывает, что любопытный шаблон рекурсивного шаблона или CRTP можно использовать для устранения необходимости проверки типа во время выполнения, если вы знаете, с каким типом имеете дело. Он также освещает проблему и решение метода, поэтому вам обязательно следует проверить его ответ.

Вот еще один ресурс по CRTP, который мне показался полезным: Стоимость динамической (виртуальные вызовы) и статической (CRTP) отправки в C++

class Expr {
public:
    enum class Kind : unsigned char {
        Int_lit_expr,
        Neg_expr,
        Add_expr,
        Sub_expr,
    };

    [[nodiscard]] virtual Kind get_kind() const noexcept = 0;

    [[nodiscard]] virtual bool
    is_unary() const noexcept {
        return false;
    }

    [[nodiscard]] virtual bool
    is_binary() const noexcept {
        return false;
    }
};


class Unary_expr : public virtual Expr {
public:
    [[nodiscard]] bool
    is_unary() const noexcept final {
        return true;
    }

    [[nodiscard]] Expr const*
    get_expr() const noexcept {
        return m_expr;
    }

protected:
    explicit Unary_expr(Expr const* p_expr) noexcept : m_expr{p_expr} {}

private:
    Expr const* const m_expr;
};


class Binary_expr : public virtual Expr {
public:
    [[nodiscard]] bool
    is_binary() const noexcept final {
        return true;
    }

    [[nodiscard]] Expr const*
    get_lhs() const noexcept {
        return m_lhs;
    }

    [[nodiscard]] Expr const*
    get_rhs() const noexcept {
        return m_rhs;
    }

protected:
    Binary_expr(Expr const* p_lhs, Expr const* p_rhs) noexcept : m_lhs{p_lhs}, m_rhs{p_rhs} {}

private:
    Expr const* const m_lhs;
    Expr const* const m_rhs;
};


class Add_expr final : public Binary_expr {
public:
    Add_expr(Expr const* p_lhs, Expr const* p_rhs) noexcept : Binary_expr{p_lhs, p_rhs} {}

    [[nodiscard]] Kind get_kind() const noexcept final {
        return Kind::Add_expr;
    }
};


int main() {
    auto const add = Add_expr{nullptr, nullptr};
    Expr const* const expr_ptr = &add;

    if (expr_ptr->is_unary()) {
        // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores): it is just an example
        auto const* const expr = dynamic_cast<Unary_expr const* const>(expr_ptr)->get_expr();

    } else if (expr_ptr->is_binary()) {
        // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores): it is just an example
        auto const* const lhs = dynamic_cast<Binary_expr const* const>(expr_ptr)->get_lhs();
    
        // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores): it is just an example
        auto const* const rhs = dynamic_cast<Binary_expr const* const>(expr_ptr)->get_lhs();
    }
}
person user1032677    schedule 22.08.2020

Все эти аргументы замечательны, но есть случаи, когда эти решения неприменимы. Одним из примеров является опытная спецификация JNI. Sony добавила оболочку C++ в официальный Java Native Interface в последнюю очередь. Например, они определяют метод GetObjectField(), который возвращает jobject. Но если поле является массивом, вы должны привести его к jbyteArray, например чтобы иметь возможность использовать GetArrayLength().

Невозможно использовать dynamic_cast с JNI. Альтернативы: либо приведение в стиле C, либо static_cast, и я считаю, что последнее безопаснее или, по крайней мере, чище, чем

(jbyteArray)env->CallObjectMethod(myObject, toByteArray_MethodID);

Чтобы подавить предупреждение в Android Studio для одной строки, используйте NOLINT:

auto byteArray = static_cast<jbyteArray>(env->CallObjectMethod(myObject, toByteArray_MethodID)); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)

Или, установите

#pragma ide diagnostic ignored "cppcoreguidelines-pro-type-static-cast-downcast"

для файла или блока

person Alex Cohn    schedule 17.01.2021