Как избежать указателей при использовании динамического полиморфизма?

Долгое время я воспринимал указатели, new и delete несколько ненужными в С++, если не иметь дело с долгоживущими объектами, причем ссылки являются более чистой альтернативой, которая лучше подходит для модели RAII. Тем не менее, я все еще не могу установить, как избежать указателей при использовании динамического полиморфизма в С++.

Предположим, что у нас есть эта класс:

class A
{
public:
    virtual void a() const = 0;
};

class B : public A
{
    virtual void a() const
    {
        std::cout << "B";
    }
};

class C : public A
{
    virtual void a() const
    {
        std::cout << "C";
    }
};

void invoke(const A& obj)
{
    obj.a();
}

int main()
{
    B b;
    invoke(b); // Prints B
}

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

Если бы я хотел, чтобы тип b зависел от чего-то другого, мне пришлось бы использовать указатели:

int main()
{
    A* a;
    if (something)
        a = new B;
    else
        a = new C;

    invoke(*a); // Prints B
    delete a;
}

Это выглядит ужасно для меня. Конечно, я мог бы использовать умные указатели:

int main()
{
    std::unique_ptr<A> a;
    if (something)
        a.reset(new B);
    else
        a.reset(new C);
    invoke(*a); // Prints B
}

Но интеллектуальные указатели - это просто оболочки для указателей.

Я хотел бы знать, есть ли способ избежать этого и использовать полиморфные классы без использования указателей.

Ответ 1

Вы не можете избежать указателей на это. Если вам это не нравится, С++ не будет для вас языком, потому что если вы хотите сделать что-либо полиморфное, вам придется использовать указатели, чтобы пройти мимо самых простых приложений. Построение объектов в куче, т.е. С помощью new, - это то, как вы избегаете охваченного временем объекта построенных стекю объектов, которые вы должны делать, если хотите сделать вещи внутри условных ветвей, а затем назначить их переменным в родительской области - если вам не нужно делать то, что вам также не нужен полиморфизм, потому что ваши типы определяются во время компиляции. Нет никакого способа обойти это.

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

Ответ 2

Учитывая ваш последний пример, вы можете избежать использования указателей (хотя я действительно не вижу большой проблемы с интеллектуальными указателями)

struct AHolder {
  std::shared_ptr<A> a;
  operator A const&() const {
    return *a;
  }
  AHolder(bool something)
  : a(something?std::make_shared<B>():std::make_shared<C>()) {
  }
};

Это позволяет использовать держатель как если бы это был фактический объект:

int main() {
  AHolder a(true); // or whatever argument you like
  invoke(a);
}

Ответ 3

Я не знаю, почему вы хотите избежать указателей любого рода любой ценой. Абсолютно хорошо использовать std::unique_ptr/std::shared_ptr (когда есть необходимость, конечно). Умные указатели не являются "обтекателями" вокруг указателей. Они позволяют выбирать между различной семантикой и использовать разные виды использования.

Ответ 4

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

std::unique_ptr<Message> parse_message(char const * buf, std::size_t len)
{
    if (buf[0] == 'A') { return make_unique<RequestMsg>(buf + 1, len - 1); }

    if (buf[0] == 'R') { return make_unique<AnswerMsg>(buf + 1, len - 1); }

    return nullptr;
}

Использование:

auto msgptr = parse_msg(buf, len);

(Вам нужно будет иметь определение make_unique где-то, чего, к сожалению, нет в текущей стандартной библиотеке, но в конечном итоге будут внесены изменения.)

Ответ 5

Я хотел бы знать, есть ли способ избежать этого и использовать полиморфные классы без использования указателей.

Нет, вот как С++ был разработан в первую очередь. Чтобы избежать нарезки, вам нужно использовать указатели или ссылки.

Ответ 6

Нет никакого способа, если вам нужно динамически создавать объекты и удалять их из области действия. Лучшее, что вы можете сделать здесь, - скрыть создание объектов в factory и интеллектуальных указателях.

Также вы можете рассматривать статические объекты в области функций, но это нехорошее решение - вы можете создать только ограниченное количество объектов таким образом:

template <class T, int Instance> T& create()
{
   static T instance;
   return instance;
}

MyClass& obj1 = create<MyClass, 1>();
MyClass& obj2 = create<MyClass, 2>(); 
MyClass& obj3 = create<MyClass, 3>(); 
// etc

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