Как использовать виртуальные функции в производных объектах без новых? - PullRequest
2 голосов
/ 17 марта 2020

Я хотел бы использовать виртуальные функции объектов разных классов (производных от одного базового класса) без а) конструирования всех объектов или б) использования новых. Пожалуйста, посмотрите пример кода:

#include <iostream>
class A{
    public:
    virtual void p(void){std::cout << "Im A" << std::endl;};
};
class B : public A{
    public:
    virtual void p(void) override {std::cout << "Im B" << std::endl;};
};
class C : public A{
    public:
    virtual void p(void) override {std::cout << "Im C" << std::endl;};
};

int main(){
    bool cond = true;   // some condition
    A* o1;
    if (cond) o1 = new B(); else o1 = new C();
    o1->p();    // will call correct p(), i.e. either B::p or C::p but not A::p

    A o2 = B();
    o2.p();     // will call A::p

    A* o3;
    B tmp1; C tmp2; // construct both objects altough only one is needed
    if (cond) o3 = &tmp1; else o3 = &tmp2;
    o3->p();    // will call correct p(), i.e. either B::p or C::p but not A::p

    A* o4;
    if (cond) {B tmp; o4 = &tmp;} else {C tmp; o4 = &tmp;}  // uses address of local variable
    o4->p();    // will call correct p(), i.e. either B::p or C::p but not A::p

    return 0;
}

Я хочу поведение o1, но без вызова new. o2 не работает (вызывает функцию базового класса, и если базовый класс является абстрактным, он вообще не работает). o3 работает, но создает все различные объекты, хотя нужен только один. o4 вроде "работает", но использует ссылку на локальную переменную вне ее области действия.

Каков будет правильный / лучший / современный способ C ++ сделать это?

Ответы [ 6 ]

2 голосов
/ 17 марта 2020

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

void do_stuff(A& obj)
{
}

int main()
{
    bool cond = true;   // some condition

    if (cond)
    {
       B tmp;
       do_stuff(tmp);
    }
    else
    {
       C tmp;
       do_stuff(tmp);
    }

    return 0;
}
2 голосов
/ 17 марта 2020

Как использовать виртуальные функции в производных объектах без новых

Примерно так:

B b;
C c;

A& a = cond ? b : c;
a.p();

без а) построения всех объектов

Вы также можете сделать это:

if (cond) {
    B b;
    b.p();
} else {
    C c;
    c.p();
}

На данный момент не имеет значения, является ли функция виртуальной, поскольку мы используем stati c dispatch. Что лучше, чем динамическая c диспетчеризация.


o4 вроде "работает", но использует ссылку на локальную переменную вне ее области действия .

Т.е. поведение программы не определено, т.е. она не работает вообще.

1 голос
/ 17 марта 2020

Если использование размещения new приемлемо, вы можете использовать объединение для хранения хранилища, а затем создать правильный член на основе условия:

union U {
    U() { }
    B b;
    C c;
};

int test(bool cond) {
    U u;
    A *a2;
    if (cond)
        a2 = new (&u.b) B;
    else
        a2 = new (&u.c) C;

    a2->p();
    return 0;
}

Конструктор в объединении необходим, поскольку конструктор по умолчанию будет неявно удален из-за наличия нетривиальных конструкторов по умолчанию для членов объединения.

Вам также придется вручную уничтожать ваши объекты, когда вы закончите.

0 голосов
/ 18 марта 2020

Если вы хотите избежать new, главным образом, чтобы избежать дополнительного кода очистки и потенциальных ошибок от висячих указателей, двойных удалений и т. Д. c., Просто используйте std::unique_ptr:

std::unique_ptr<A> o1;
if (cond)
    o1 = std::make_unique<B>();
else
    o1 = std::make_unique<C>();
o1->p();

This будет делать то же самое, что и код, использующий new, но за кадром, а также delete для вас, когда o1 выходит за рамки.

Если у вас есть код, критичный для производительности Узкое место и желание избежать накладных расходов на выделение и освобождение кучи памяти, используемых new, все усложняется, но вы можете сделать это с помощью чего-то вроде:

using B_or_C_type = std::variant<B,C>;
auto o5 = cond ? B_or_C_type{B{}} : B_or_C_type{C{}};;
std::visit(std::mem_fn(&A::p), o5);

В типичных системах этот код позволит избежать использовать любую память кучи вообще, вместо этого используя стековую память, достаточно большую, чтобы вместить большее из B или C. У std::variant есть логика c, действующая как union, но проверяющая, что на самом деле используется только правильный тип. std::visit применяет любой функтор к варианту при условии, что функтор может быть вызван с каждым типом в варианте. Обратите внимание, что если вы определяете переменную variant без инициализатора, он создает объект первого типа, хотя позже его можно переназначить объекту другого типа. (Если создание этого объекта по умолчанию невозможно или его следует избегать, вы можете использовать std::monostate в качестве первого фиктивного типа - но затем использование std::visit становится сложнее.)

0 голосов
/ 17 марта 2020

Если вы беспокоитесь об использовании new из-за управления памятью, просто используйте std::shared_ptr вместо:

int main(){
    bool cond = true;   // some condition
    std::shared_ptr<A> o1;

    if(cond) {
        o1 = std::make_shared<B>();
    } else {
        o1 = std::make_shared<C>();
    }

    o1->p();    

    return 0;
}

Отпечатки:

Im B

Рабочий пример


Примечание: Первоначально я собирался предложить std::unique_ptr вместо этого, однако я не сделал думаю, что они были копируемыми. Но я был неправ. std::unique_ptr с тоже будет работать.


Примечание 2: вам потребуется C ++ 14 или более поздняя версия, чтобы использовать std::make_shared() или std::make_unique, но new будет работать в C ++ 11. Это нарушает ваше правило не использовать new, но, по крайней мере, управляет памятью для вас.

0 голосов
/ 17 марта 2020

Для начала этот фрагмент кода

A* o4;
if (cond) {B tmp; o4 = &tmp;} else {C tmp; o4 = &tmp;}  // uses address of local variable
o4->p();    // will call correct p(), i.e. either B::p or C::p but not A::p

имеет неопределенное поведение, потому что вне оператора if указатель o4 недопустим из-за того, что локальный объект tmp не является живым вне областей действия if под-оператор.

Что касается этого фрагмента кода

A o2 = B();
o2.p();     // will call A::p

, то вам следует использовать ссылку на объект типа B. Например,

B b;
A &o2 = b;
o2.p();

As для фрагмента кода с указателем o1 в любом случае указанные объекты должны существовать. Либо они будут созданы динамически, как в вашем примере, либо вы можете создать оба локально, а затем в зависимости от условия установить указатель на один из них.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...