Являются ли нечистые виртуальные функции с параметрами плохой практикой?

У меня есть базовый класс с дополнительной виртуальной функцией

class Base {
    virtual void OnlyImplementThisSometimes(int x) {}
};

Когда я компилирую это, я получаю предупреждение о неиспользуемом параметре x. Есть ли другой способ реализовать виртуальную функцию? Я переписал это так:

class Base {
    virtual void OnlyImplementThisSometimes(int x) 
    {
        x = 0;
    }
};

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

class Derived : public Base {
    void OnlyImplementThisSometimes(int x, int y) { // some code }
};

Derived d;
Base *b = dynamic_cast<Base *>(&d);
b->OnlyImplementThisSometimes(x); // calls the method in the base class

Метод базового класса был вызван, потому что я реализовал производную функцию с параметром «int y», но об этом нет предупреждения. Это просто распространенные ловушки в С++ или я неправильно понял виртуальные функции?


person Matthew Smith    schedule 03.11.2008    source источник


Ответы (10)


Игнорируя проблемы дизайна, вы можете обойти предупреждение компилятора о неиспользуемой переменной, опустив имя переменной, например:

virtual void OnlyImplementThisSometimes(int ) { }

Ошибочная реализация неправильной сигнатуры метода при попытке переопределить виртуальную функцию — это то, с чем вам нужно быть осторожным в C++. Такие языки, как C#, обходят это с помощью ключевого слова override.

person nlativy    schedule 04.11.2008
comment
Другой способ сделать это (тот же эффект) — закомментировать имя переменной внутри строки, например: int /*x*/. Если имя переменной является описательным (как и должно быть), это поможет читателям. - person Nick; 04.11.2008
comment
@Nick: я твердо верю в строгую безопасность типов, когда тип описывает намерение (и предотвращает путаницу с аргументами). Если вы (в основном) кодируете так, имена параметров не важны. - person Sebastian Mach; 12.12.2011
comment
Последний абзац можно отредактировать, чтобы отразить ключевое слово override в C++. - person Rotem; 29.05.2016

Мы определяем макрос _unused как:

#define _unused(x) ((void)x)

Затем определите функцию как:

virtual void OnlyImplementThisSometimes(int x) { _unused( x );}

Это не только предотвращает жалобы компилятора, но и делает очевидным для любого, кто поддерживает код, что вы не забыли об x - вы намеренно его игнорируете.

person Graeme Perrow    schedule 04.11.2008
comment
Если вы программируете с помощью Qt, вы можете использовать макрос Q_UNUSED, который идентичен этому предлагаемому решению. - person Jonny Dee; 20.04.2012

Зачем определять его в базовом классе? Если базовый класс не собирается использовать этот метод, просто определите его как виртуальный метод в производном классе.

Или реализация по умолчанию может вызвать исключение

person Chris Thompson    schedule 04.11.2008

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

person tvanfosson    schedule 04.11.2008
comment
И если вы можете придумать реализацию, которая обычно правильна, но не всегда, то вы можете предоставить реализацию чистой виртуальной функции, и в обычном случае производный класс просто вызывает ее. - person Steve Jessop; 04.11.2008

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

int func(int x)
{
   (void) x;
}
person EvilTeach    schedule 04.11.2008
comment
Строго говоря, это не говорит компилятору, что он не используется и SHUTUP. Он использует это. - person Steve Jessop; 04.11.2008

Это довольно часто встречается в моем коде. Например, у меня есть классы, предназначенные для однопоточной работы и многопоточные. Есть много общих процедур и данных. Я поместил все это в базовый класс (который также имеет пару чистых виртуальных).

Я реализую две пустые виртуальные функции в базовом классе: Init() и Cleanup(). Однопоточный производный класс их не реализует, а многопоточный делает.

У меня есть фабричная функция, создающая соответствующий производный класс, а затем возвращающая указатель. Клиентский код знает только о типе базового класса и вызывает Init() и Cleanup(). Оба сценария работают правильно.

Конечно, могут быть и другие хорошие предложения о том, как это сделать, но эта идиома хорошо работает для большей части моего кода.

person Foredecker    schedule 04.11.2008
comment
Я считаю, что переопределение чистой виртуальной функции в производном классе и заставление ее просто ничего не делать приводит к более ясному и простому в использовании коду, чем выполнение этого в базовом классе без переопределения в производном классе. Это менее вероятно для неправильного использования, потому что пользователь класса должен думать об этом. - person foraidt; 04.11.2008

Это неплохая практика, и это обычная идиома для указания частей класса, которые необязательны для реализации.

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

class mouse_listener{
public:
    virtual ~mouse_listener() {}

    virtual void button_down(mouse_button a_Button) {}
    virtual void button_up(mouse_button a_Button) {}
    virtual void scroll_wheel(mouse_scroll a_Scroll) {}
    virtual void mouse_move_abs(math::point a_Position) {}
    virtual void mouse_move_rel(math::point a_Position) {}
};
person Jasper Bekkers    schedule 04.11.2008

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

Base *b = &d;

Сработает так же хорошо, вместо этого следует использовать dynamic_cast<>, когда вы выполняете приведение вниз, то есть от основы к производной:

if((Derived *d = dynamic_cast<Derived *>(b)) != 0)
{
  // use d
}

(И, конечно же, в случае приведения вниз static_cast<> обычно также работает.)

person Andreas Magnusson    schedule 04.11.2008
comment
Я могу вспомнить по крайней мере один случай, когда необходимы динамические приведения типов вверх: если вы наследуете параметр шаблона и вам нужно вызывать определенные функции на основе базового типа. В моем случае это было примерно так: if(fixture *f = dynamic_cast‹fixture*›(this)){f-›set_up();} - person Jasper Bekkers; 04.11.2008
comment
Из-за того, как настроена структура, я не знаю на тот момент, является ли тестовый пример производным от фикстуры или нет: сам тестовый пример происходит от аргумента шаблона. - person Jasper Bekkers; 04.11.2008

Попробуй это:

class Base {
    virtual void OnlyImplementThisSometimes(int x) = 0;
};

Прошло некоторое время с тех пор, как я делал подобные вещи, но я считаю, что именно так вы объявляете виртуальную функцию.

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

person Community    schedule 05.11.2008

Самый простой ответ на это показан ниже:

class Base {
    virtual void OnlyImplementThisSometimes(int x) { x;}
};

Простая ссылка на переменную, которая абсолютно ничего не делает, удалит все предупреждения (во всяком случае, из VC++ на самом высоком уровне).

person Tim Ring    schedule 04.11.2008
comment
Во-первых, это все еще предупреждает в GCC с -Wall (утверждение не имеет никакого эффекта). Во-вторых, это не самый простой ответ — опускание имени параметра. - person Steve Jessop; 04.11.2008