Принуждение неквалифицированных имен к зависимым значениям

Есть шаблон в некотором устаревшем тестовом фреймворке, который зависит от двухфазного поиска Visual С++, вызывающего головные боли при переносе на другие компиляторы. Есть множество решений, которые я знаю, чтобы исправить проблему, но все они требуют "обширных" структурных изменений.

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

Рисунок показан в этом примере:

#include <cstdio>

// global "system" function to test; generally something like `fopen` in a real test
const char* GetString() { return "GLOBAL"; }

// provides no overrides of the standard system functions being tested
struct NoOverrides {};

// set of functions overriding the system functions being tested
struct TestOverrides {
  // if this were `fopen` this might be a version that always fails
  static const char* GetString() { return "OVERRIDE"; }
};

// test case
template <typename Overrides>
struct Test : private Overrides {
  void Run() {
    // call to GetString is not dependent on Overrides
    printf("%s\n", GetString());
  }
};

int main() {
  // test with no overrides; use the system functions
  Test<NoOverrides> test1;
  test1.Run();

  // test with overrides; use test case version of system functions
  Test<TestOverrides> test2;
  test2.Run();
}

Идея состоит в том, что существуют глобальные функции, обычно что-то определенное в заголовке системы (например, функция ANSI C или функция, предоставляемая ОС). Затем существует тип, который определяет группу альтернативных версий этого как статические функции-члены. Тест может наследовать либо от типа с этими альтернативными версиями, либо с типом, который не имеет альтернатив.

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

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

#include <cstdio>

const char* GetString() { return "GLOBAL"; }

// wrappers to invoke the system function versions
struct NoOverrides {
  static const char* GetString() { return ::GetString(); }
};

struct TestOverrides {
  static const char* GetString() { return "OVERRIDE"; }
};

template <typename Overrides>
struct Test {
  void Run() {
    // call to GetString is made dependent on template type Overrides
    printf("%s\n", Overrides::GetString());
  }
};

int main() {
  // test with the default overrides; use the system functions
  Test<NoOverrides> test1;
  test1.Run();

  Test<TestOverrides> test2;
  test2.Run();
}

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

Ответы

Ответ 1

Здесь моя наименее интрузивная версия без SFINAE. Протестировано с помощью Apple LLVM 5.1 и g++ 4.8.2. Это только одна реализация общей идеи, описанной ниже кода.

#include <cstdio>
// global "system" function to test; generally something like `fopen` in a real test
const char* GetString() { return "GLOBAL"; }

// list all global functions that could possibly be overridden by function pointers
// (initialize them with the original function) and provide template overriding function.
struct PossibleOverridesList
{
    decltype(&::GetString) GetString = &::GetString;

    template<typename O>
    void setOverrides() {
        O::setMyOverrides(this);
    };

};

// provides no overrides of the standard system functions being tested
// (setOverrides method does nothing)
struct NoOverrides { static void setMyOverrides(PossibleOverridesList* ol){}; };

// set of functions overriding the system functions being tested
// (setOverrides method sets the desired pointers to the static member functions)
struct TestOverrides {
  // if this were `fopen` this might be a version that always fails
  static const char* GetString() { return "OVERRIDE"; }
  static void setMyOverrides(PossibleOverridesList* ol) { ol->GetString = &GetString; };
};

// test case (inheritance doesn't depend on template parameters, so it gets included in the lookup)
struct Test : PossibleOverridesList {
  void Run() {
    printf("%s\n", GetString());
  }
};

int main() {
  // test with no overrides; use the system functions
  Test test1;
  test1.setOverrides<NoOverrides>();
  test1.Run();

  // test with overrides; use test case version of system functions
  Test test2;
  test2.setOverrides<TestOverrides>();
  test2.Run();
}

Моя идея такова: поскольку я понимаю, что вы не хотите изменять код внутри Run(), нет способа поиска немодифицированного имени из класса шаблона. Поэтому некоторый (вызываемый) объект-заполнитель должен быть в области видимости, когда GetString просматривается, внутри которого мы все еще можем решить, какую функцию вызывать (если GetString() внутри Run() связывается с глобальным GetString() один раз, мы не можем сделать что-нибудь позже, чтобы изменить его). Это может быть функция в окружающем пространстве имен или указатель объекта/функции функции/функции в базовом классе (не шаблон). Я выбрал последнего здесь (с классом PossibleOverridesList), чтобы как можно ближе приблизиться к исходному коду. Этот заполнитель по умолчанию вызывает исходную версию функции, но может быть изменен классами, аналогичными классам переопределения. Единственное отличие состоит в том, что для этих классов переопределения нужен дополнительный метод setMyOverrides(PossibleOverridesList*), который соответственно устанавливает указатели на функции в классе-заполнителе.

Изменения в главном не являются строго необходимыми. В моем коде вы должны вызвать setOverrides<...OverriderClass...>() вместо указания OverriderClass в объявлении. Но должно быть легко написать класс шаблона-оболочки, который наследует от Test и сохраняет исходный синтаксис, внутренне вызывая метод setOverrides с его аргументом шаблона сам по себе во время построения.

Приведенный выше код имеет несколько преимуществ перед решением предоставления оберток в классе NoOverrides (ваше предложение - ваш вопрос):

  • С обертками, если у вас много переопределенных классов со многими переопределенными функциями, вам придется всегда предоставлять оболочку для всех функций, которые вы не хотите переопределять. Здесь есть только один глобальный список функций, которые могут быть переопределены, PossibleOverridesList. Это легко заметить, если вы забыли его, потому что попытка установить этот элемент в методе setMyOverrides соответствующего класса переопределения не будет компилироваться. Таким образом, есть только два места, чтобы что-то изменить, если вы добавите другой переопределенный метод в любой класс переопределения.
  • Вам не нужно воспроизводить подпись функции для обертки системной функции
  • Вам не нужно изменять код внутри Run(), чтобы использовать функции из унаследованного класса.
  • Я не знаю, может ли в вашем исходном сложном коде VС++ Test действительно наследовать от нескольких классов переопределения. Но если это возможно, пакетное решение не будет работать, так как вы не знаете, как квалифицировать метод (вы не знаете, из какого унаследованного класса он приходит). Здесь вы можете легко вызвать метод setOverrides() несколько раз в тестовом объекте для последовательного замены определенных функций.

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


EDIT: Я просто придумал еще менее навязчивую версию, которая нуждается только в дополнительном классе PossibleOverridesList, но без изменений в Run(), main(). Самое главное, нет необходимости в методах setOverrides, а шаблонное наследование Test<Override> с исходного кода сохраняется!

Хитрость здесь заключается в использовании виртуальных методов вместо статических и использования виртуального наследования. Таким образом, вызов функции GetString() в Run() может привязываться к виртуальной функции в PossibleOverridesList (поскольку это наследование не зависит от параметра шаблона). Затем во время выполнения вызов отправляется в самый производный класс, то есть в класс переопределения. Это однозначно из-за виртуального наследования, поэтому на самом деле в классе присутствует только один объект PossibleOverridesList.

Итак, в итоге, минимальные изменения в этой версии:

  • define PossibleOverridesList со всеми функциями, которые могут быть переопределены (как функции виртуальных членов), перенаправлением на исходную системную функцию.
  • изменить методы-члены в классах переопределения от static до virtual.
  • пусть все переопределить, а классы Test наследуют фактически от OptionOverridesList в дополнение к любому другому наследованию. Для NoOverride это не является строго необходимым, но приятным для согласованного шаблона.

Здесь код (опять же, проверенный с Apple LLVM 5.1 и g++ 4.8.2):

#include <cstdio>
// global "system" function to test; generally something like `fopen` in a real test
const char* GetString() { return "GLOBAL"; }

// list all global functions that could possibly be overridden by function pointers
struct PossibleOverridesList
{
    virtual const char* GetString() {return ::GetString();};
};

// provides no overrides of the standard system functions being tested
struct NoOverrides : virtual PossibleOverridesList { };

// set of functions overriding the system functions being tested
struct TestOverrides : virtual PossibleOverridesList {
  // if this were `fopen` this might be a version that always fails
  virtual const char* GetString() { return "OVERRIDE"; }
};

// test case (inheritance from first class doesn't depend on template parameter, so it gets included in the lookup)
template <typename Override>
struct Test : virtual PossibleOverridesList, Override {
  void Run() {
    printf("%s\n", GetString());
  }
};

int main() {
  // test with no overrides; use the system functions
  Test<NoOverrides> test1;
  test1.Run();

  // test with overrides; use test case version of system functions
  Test<TestOverrides> test2;
  test2.Run();
}