Category Archives: С/C++

Идея насчет синтаксиса обработки исключений

Written by elwood

Часто приходится писать обработчики исключений, которые делают одно и то же. Например, в следующем кусочке кода нам необходимо среагировать на исключения типа Exception1 и Exception2 записью в лог-файл :

try {
  // Блок, который может вызвать исключения Exception1, Exception2
  // или исключение любого другого типа
} catch (Exception1 exc) {
  logger.WarnException("An exception has been occured : {0}", exc);
} catch (Exception2 exc) {
  logger.WarnException("An exception has been occured : {0}", exc);
}

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

try {
  // 
} catch (Exception1, Exception2 exc as Exception) {
  // exc имеет тип Exception
}

(more…)

Проблема с преобразованием к самому себе в GCC

Written by elwood

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

const HandlePointer<lib3dsfile>& file = loadFile();

Функция loadFile() возвращала HandlePointer<Lib3dsFile> по значению, и по идее это значение должно было быть привязано (bind) к константной ссылке. Время жизни временного (temporary) объекта в этом случае по Стандарту должно быть продлено и временный объект должен быть жив до тех пор, пока жива константная ссылка. Однако, добавив диагностических логов, я с удивлением отметил, что деструктор временного объекта в данном случае вызывается сразу после выполнения этой строки, не дожидаясь выхода ссылки из области видимости. Замена ссылки на объект-копию магическим образом исцеляло программу, временный объект уничтожался после вызова конструктора копий, и программа работала как раньше.

Добавив еще чуть больше логов, я удивился еще раз, увидев, что в указанном выражении вызывается
оператор преобразования HandlePointer<T>::operator T& () , то есть объект преобразовывался к самому себе, однако при этом объект уже не привязывался к константной ссылке, а уничтожался автоматически после вычисления полного выражения (full expression).

Дома для проверки написал тестовую программу, которая объявляла класс Test и оператор преобразования к типам Test& и const Test& :

class Test
{
public:
	Test() { printf("Default constructor\r\n"); }
	~Test()	{ printf("Destructor\r\n");	}
	Test(const Test& copy) { printf("Copy constructor\r\n"); }
	Test& operator= (const Test& copy) {
		printf("operator =\r\n");
		return *this;
	}
	//
	operator Test& () {
		printf("operator Test&\r\n");
		return *this;
	}
	operator const Test& () const {
		printf("operator const Test&\r\n");
		return *this;
	}
}
 
Test someFunc()
{
	return Test();
}
 
int main()
{
	{
		printf("Begin\r\n");
		const Test& test = someFunc();
		printf("End\r\n");
	}
}

Натравив на него компиляторы Visual C++ и GCC, посмотрел на результаты. Произошло все так же, как и в случае шаблонных классов :

Вывод программы, скомпилированной Visual C++ :
Begin
Default constructor
End
Destructor

Вывод программы, скомпилированной GCC :
Begin
Default constructor
operator Test&
Copy constructor
Destructor
operator Test&
Destructor
End

На обилие конструкторов копий и деструкторов в выводе GCC можно не обращать внимания – это
всего лишь говорит о том, что GCC не выполнил здесь RVO (Return Values Optimization). Главное –
что в Visual C++ временный объект живет до конца блока { }, а в GCC уничтожается сразу.

Почему поведение GCC здесь мне представляется неверным ? Допустим, у вас есть класс. У класса есть
дефолтные операторы преобразования их к ссылке и к константной ссылке, генерируемые компилятором автоматически. И временные объекты успешно привязываются к константным ссылкам, продлевая их время жизни. Однако, если вы вдруг (как сейчас я) захотите написать свою версию такого оператора (что в принципе, бессмысленно, но в контексте использования шаблонов смысл приобретает), то вы уже не сможете действовать по старым правилам. Таким образом, явное определение пользовательского оператора преобразования (такого же, как и дефолтный, по сути) изменяет поведение объектов и семантику в целом.

В общем, сбило порядочно меня все это с толку, и я обратился за помощью на блог Алены С++ (в котором как раз недавно читал про RVO и привязку временных объектов к const& ) и на RSDN.
На следующий день поступили комментарии, позволяющие разобраться в том, что же все-таки происходит и какой из компиляторов в данном случае прав.

Краткое резюме беседы (для тех, кому лениво читать обсуждение) :

Здесь мы видим следствие изначального внутреннего противоречия в Стандарте 98 (см ветку на RSDN).
Стандарт был скорректирован в дальнейшем в ходе внесения правок и уточнений.
На данный момент корректным поведением является поведение, при котором компилятор не должен вызвать переопределенные операторы преобразования в случае, если это преобразование выполняется к ссылке на тот же тип или к объекту того же типа:

12.3.2/1

A conversion function is never used to convert a (possibly cv-qualified)
object to the (possibly cv-qualified) same object type (or a reference to it)

Таким образом, компилятор GCC на данный момент не соответствует исправленному Стандарту.
Что ж, печально, придется использовать вместо неявных преобразований явный вызов функции.
Хотя, возможно, есть способ, который бы позволил ограничить шаблонный метод преобразования таким образом, чтобы он не использовался, когда тип T идентичен типу U ? Я попробовал пошаманить с
шаблонами и SFINAE, но пока ничего путного не вышло. Если у кого-нибудь есть такое решение,
было бы интересно о нем узнать.

Неявное приведение между родственными шаблонами С++

Written by elwood

Сегодня столкнулся с интересной проблемой и нашел не менее интересный способ решения. Как всем известно, если мы имеем иерархию классов class Derived : public Base, и шаблонный класс наподобие Template<typename T>, то классы, которые будут сгенерированы при инстанцировании шаблонов Template<Base> и Template<Derived> не имеют ничего общего. То есть мы не сможем привести тип Template<Derived> к типу Template<Base>, но ведь часто это просто необходимо ! В данном случае не работает ни один из способов приведения (даже reinterpret_cast). Приходится действовать обходным путем : брать адрес у объекта, приводить его к типу указателя на объект искомого типа, и выполнять разыменование. Выглядит это, мягко говоря, не слишком элегантно :

  Template<Derived> derived;
  Template<Base>& base = *static_cast<Template<Base>*> (static_cast<void*> (&derived));

В принципе, таким способом можно выполнить приведение Template<Derived> к шаблону с любым аргументом-типом, главное следить, чтобы приведенный экземпляр вел себя корректно (вызывал правильный деструктор, обладал той же бинарной структурой итд). Как можно усовершенствовать этот код ? Идею подсказал мне коллега, напомнив про имеющуюся в арсенале С++ возможность перегрузки оператора приведения типов. В самом деле, почему нет ? Ведь мы можем определить оператор преобразования типа, и к тому же сделать его шаблонной функцией, которая будет принимать тип, в который необходимо выполнить преобразование. С учетом того, что для шаблонных функций работает механизм автоматического вывода аргументов шаблона, это решение кажется все более изящным:

  template<typename T>
  class Template
  {
  public:
    template <typename U>
    operator Template<U>& ()
    {
      return (*static_cast<Template<U>*> (static_cast<void*> (this)));
    }
  // ...
  };

Работает ! И у нас даже нет необходимости явно указывать шаблонные параметры для оператора приведения, поскольку компилятор выводит их автоматически из контекста использования :

  Template<Base>& base = derived;

К сожалению, используя этот способ, мы теряем контроль над происходящим, поскольку приведение будет успешным для любого аргумента шаблона U. Было бы замечательно добавить некие ограничения на эту операцию.. Но – опять же – в чем проблема ? Ведь мы можем добавить в код шаблонной функции-оператора кусочек кода, который будет выполнять преобразование указателя T* в указатель U*. И если это приведение корректно, то шаблонная функция успешно скомпилируется, в противном случае инстанцирования произведено не будет. Т.е. компилятор будет решать – насколько корректно приведение внешних шаблонов-классов по корректности приведения их шаблонных аргументов. Заодно можно сделать этот код никогда не выполняющимся, поскольку помимо верификации типов он никакой смысловой нагрузки не несет.

  template <typename U>
  operator Template<U>& ()
  {
    if (0) {
      T* derivedTypePtr = NULL;
      U* baseTypePtr = derivedTypePtr;
      (void) baseTypePtr;  // А это может пригодиться для того, чтобы подавить
                           // предупреждения компилятора о неиспользуемой переменной
    }
    return (*static_cast<Template<U>*> (static_cast<void*> (this)));
  }

Таким образом – мы получаем безопасный по отношению к типам код, который полностью прозрачен для пользователя. Далее – вспоминаем про константность : следующий код не скомпилируется, даже если derived был изначально объявлен как const.

  const Template<Base>& base = derived;

Чтобы устранить это досадное недоразумение, достаточно написать константную версию оператора :

  template <typename U>
  operator const Template<U>& () const
  {
    if (0) {
      T* derivedTypePtr = NULL;
      U* baseTypePtr = derivedTypePtr;
      (void) baseTypePtr;
    }
    return (*static_cast<const Template<U>*> (static_cast<const void*> (this)));
  }

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

  template <typename U>
  Template<U>& do_unsafe_cast()
  {
    return (*static_cast<Template<U>*> (static_cast<void*> (this)));
  }
 
  template <typename U>
  const Template<U>& do_unsafe_cast() const
  {
    return (*static_cast<const Template<U>*> (static_cast<const void*> (this)));
  }

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

  Template<Derived>& derived = base.do_unsafe_cast<Derived>();

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

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

PS. Не думаю, что описанный подход будет откровением для знатоков С++, которые используют шаблоны на полную катушку, но, надеюсь, что это будет кому-то полезно.

UPD: Внимание ! Этот код некорректно работает с компилятором GCC, поскольку GCC неверно
обрабатывает ситуации, связанные с преобразованием ссылок. В Visual C++ и Comeau все в норме.
Для того, чтобы эта особенность компилятора GCC не приводила к некорректной работе приложения,
придется избавиться от удобств перегруженных операторов преобразования и вместо них написать
аналогичные safe_cast-методы. Объяснение проблемы в следующем посте.