Какую пользу извлекает компилятор из нового ключевого слова C ++? - PullRequest
29 голосов
/ 24 сентября 2011

C ++ 11 позволит пометить классы и виртуальный метод как final , чтобы запретить их извлечение или переопределение.

class Driver {
  virtual void print() const;
};
class KeyboardDriver : public Driver {
  void print(int) const final;
};
class MouseDriver final : public Driver {
  void print(int) const;
};
class Data final {
  int values_;
};

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

Но есть ли преимущество с точки зрения компиляторов? Может ли компилятор сделать что-то другое, когда он знает, "этот класс никогда не будет производным" или "эта виртуальная функция никогда не будет переопределена"?

Для final В основном я нашел только N2751, ссылающийся на него. Просматривая некоторые обсуждения, я нашел аргументы со стороны C ++ / CLI, но нет четкого намека на то, почему final может быть полезен для компилятора. Я думаю об этом, потому что я также вижу некоторые недостатки маркировки класса final: для модульного тестирования защищенных функций-членов можно получить класс и вставить тест-код. Иногда эти классы являются хорошими кандидатами для отметки final. Эта техника была бы невозможна в этих случаях.

Ответы [ 3 ]

36 голосов
/ 24 сентября 2011

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

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

Например, если у вас есть delete most_derived_ptr, где most_derived_ptr - указатель на производный тип final, тогда компилятор может упростить вызовы деструктора virtual.

Аналогично для вызовов virtual функций-членов в ссылках / указателях на наиболее производный тип.

Я был бы очень удивлен, если бы какой-либо компилятор сделал это сегодня, но похоже, что такая вещь может быть реализована в течение следующего десятилетия или около того.в состоянии сделать вывод, что (при отсутствии friend s) вещи, отмеченные protected в final class, также фактически становятся private.

30 голосов
/ 24 сентября 2011

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

  1. нахождению указателя v-таблицы и, через него, достижению v-таблицы
  2. Нахождение указателя функции в v-таблице и выполнение через него вызова

По сравнению с прямым вызовом, где адрес функции известен заранее (и жестко закодирован с помощьюсимвол), это приводит к небольшим накладным расходам.Хорошим компиляторам удается сделать его всего на 10-15% медленнее, чем обычный вызов, что обычно незначительно, если у функции есть какой-либо символ.* Девиртуализация функциональных вызовов, как правило, является низко висящим плодом.Например, см. В C ++ 03:

struct Base { virtual ~Base(); };

struct Derived: Base { virtual ~Derived(); };

void foo() {
  Derived d; (void)d;
}

Clang получает:

define void @foo()() {
  ; Allocate and initialize `d`
  %d = alloca i8**, align 8
  %tmpcast = bitcast i8*** %d to %struct.Derived*
  store i8** getelementptr inbounds ([4 x i8*]* @vtable for Derived, i64 0, i64 2), i8*** %d, align 8

  ; Call `d`'s destructor
  call void @Derived::~Derived()(%struct.Derived* %tmpcast)

  ret void
}

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

Фактически, это оптимизировало бы следующую функцию так же приятно:

void bar() {
  Base* b = new Derived();
  delete b;
}

Однако в некоторых ситуацияхкомпилятор не может прийти к такому выводу:

Derived* newDerived();

void deleteDerived(Derived* d) { delete d; }

Здесь можно ожидать (наивно), что вызов deleteDerived(newDerived()); приведет к тому же коду, что и раньше.Однако дело обстоит не так:

define void @foobar()() {
  %1 = tail call %struct.Derived* @newDerived()()
  %2 = icmp eq %struct.Derived* %1, null
  br i1 %2, label %_Z13deleteDerivedP7Derived.exit, label %3

; <label>:3                                       ; preds = %0
  %4 = bitcast %struct.Derived* %1 to void (%struct.Derived*)***
  %5 = load void (%struct.Derived*)*** %4, align 8
  %6 = getelementptr inbounds void (%struct.Derived*)** %5, i64 1
  %7 = load void (%struct.Derived*)** %6, align 8
  tail call void %7(%struct.Derived* %1)
  br label %_Z13deleteDerivedP7Derived.exit

_Z13deleteDerivedP7Derived.exit:                  ; preds = %3, %0
  ret void
}

Соглашение может предписывать, что newDerived возвращает Derived, но компилятор не может сделать такое предположение: а что, если он вернул что-то еще производное?И, таким образом, вы видите все уродливые механизмы, задействованные в извлечении указателя v-таблицы, выборе соответствующей записи в таблице и, наконец, выполнении вызова.

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

define void @deleteDerived2(Derived2*)(%struct.Derived2* %d) {
  %1 = icmp eq %struct.Derived2* %d, null
  br i1 %1, label %4, label %2

; <label>:2                                       ; preds = %0
  %3 = bitcast i8* %1 to %struct.Derived2*
  tail call void @Derived2::~Derived2()(%struct.Derived2* %3)
  br label %4

; <label>:4                                      ; preds = %2, %0
  ret void
}

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

0 голосов
/ 20 ноября 2011

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

Например, рассмотрим этот код:

class Base
{
  public:
    virtual void foo() { }
    Base() { }
    ~Base();
};

void destroy(Base* b)
{
  delete b;
}

Многие компиляторы выдают предупреждение для не виртуального деструктора b, когда delete bнаблюдаемый.Если класс Derived унаследован от Base и имеет свой собственный деструктор ~Derived, использование destroy в динамически размещаемом экземпляре Derived обычно (в соответствии со спецификацией поведения не определено) вызывает ~Base, но этоне звоните ~Derived.Таким образом, операции очистки ~Derived не произойдут, и это может быть плохо (хотя в большинстве случаев, вероятно, не катастрофическим).

Если компилятор знает, что Base не может быть унаследовано,однако, это не проблема, что ~Base не является виртуальным, потому что никакая производная очистка не может быть случайно пропущена.Добавление final к class Base дает компилятору информацию, чтобы не выдавать предупреждение.

Я точно знаю, что использование final таким образом подавит предупреждение с Clang.Я не знаю, выводят ли здесь другие компиляторы предупреждение или учитывают ли они окончательность при определении того, следует ли выводить предупреждение.

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