*** ВНИМАНИЕ: Блог переехал на другой адрес - demin.ws ***
Показаны сообщения с ярлыком темные углы. Показать все сообщения
Показаны сообщения с ярлыком темные углы. Показать все сообщения

воскресенье, 8 августа 2010 г.

Странные скобки в С++

Недавно более часа потратил на поиск проблемы в куске кода, упрощенный вариант которого привожу ниже:

#include <iostream>
int x;
struct A {
  A(int a) {
    x = a;
  }
};
struct B {
  B(A a) {
    A local_a = a;
  }
};
int main() {
  x = 0;
  std::cout << "Case #0: " << x << std::endl;
  B b1(A(1));
  std::cout << "Case #1: " << x << std::endl;
  int t;
  t = 2;
  B b2(A(t));
  std::cout << "Case #2: " << x << std::endl;
  t = 3;
  B b3((A(t)));
  std::cout << "Case #3: " << x << std::endl;
  return 0;
}

Как вы думаете, что должна вывести эта программа? Числа 0, 1, 2 и 3 последовательно для каждого случая?

А она печатает:

Case #0: 0
Case #1: 1
Case #2: 1
Case #3: 3

Почему для случая #2 не произошло присваивание? Куда делась двойка?

Ответ на этот вопрос кроется в наличии рудиментов языка С в грамматике С++.

понедельник, 19 июля 2010 г.

Нулевые ссылки в С++

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

...
class A { virtual void f() {} };
class B {};

int main() {
  A a;
  try {
    B& b = dynamic_cast<B&>(a);
    if (&b == 0) {
      // ...
    }
  } catch (...) {
    std::cout << "Got it!" << std::endl;
  }
  return 0;
}

Я слегка выпал в осадок от уведенного, а в частности, от строки "if (&b == 0) {". До сего времени я пребывал в осознании факта, что ссылка в С++ либо существует и указывает на реальный объект, либо ее нет вообще. И если тут приведение типа к "B&" не срабатывает, то будет исключение, и управление все равно улетит в другое место, и проверять как-либо "b" бессмысленно.

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

Ну да ладно. Оставим это на откуп странным компиляторам на AIX и SUN, и людям, использующим исключения, но почему-то компилирующие с принудительным их выключением в компиляторе.

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

int& a = *(int*)0;
int main() { a = 123; }

Данный код прекрасно компилируется Студией (2010) и компилятором SUN (этот хоть предупреждение выдает), и также прекрасно падается при запуске по понятой причине.

Вы получили ссылку в качестве параметра и думаете, что она лучше чем указатель, так как ее не надо проверять на null? Зря!

суббота, 12 июня 2010 г.

return со значением для void-функции

Я как-то думал, что для void-функций оператор return не может иметь ничего, кроме пробелов, перед завершающей его точкой с запятой. Оказывается, что нет. Visual Studio съела без каких-либо жалоб вот такой код:

void v() {}
void f(){ 
  return v();
}

int main() {
  f();
}

среда, 9 июня 2010 г.

Проблемы с delete[]

Имеем следующий код:

#define T 2

class A {
  public:
    virtual ~A() { 
      p = 0;
    }
    int p;
};

class B: public A {
  int a;
};

int main() {
  A* a = new B[T];
  delete[] a;
  return 0;
}

У меня эта программа однозначно падает с "Segmentation fault" на строке "delete[] a". Проверено на Sun C++ на Солярисе, GCC на Линуксе и на FreeBSD. Вот, например, что происходит на BSD:

Program received signal SIGSEGV, Segmentation fault.
0x08048743 in main () at new_array.cpp:17
17        delete[] a;

Забавно, что под Windows в VS2008 ничего особенного не происходит.

Как я понимаю, что в этой программе принципиально важно, чтобы она падала: деструктор класса "A" должен быть виртуальным, дочерний класс "B" должен быть больше по размеру (тут есть член "a"), константа "Т" должна быть 2 или более (то есть мы должны создавать несколько экземпляров класса "B"), и деструктор класса "A" должен что-нибудь писать в свои члены (тут есть "p = 0;").

Что же тут происходит?

new[] создает массив экземплятор класса "B". Оператор же delete[] получает на вход указатель типа "A*" и начинает вызывать деструкторы элементов. Так как деструктор класса "А" виртуальный, то в ход пускается таблица виртуальных функций. Итак, отработал деструктор для первого элемента a[0]. Далее delete[] хочет получить адрес следующего элемента массиве "a". И для этого (внимание!) адрес следующего он вычисляется так: "a + sizeof(A)" (ему же на вход дали указатель типа "A*"). Но проблема в том, что sizeof(A) < sizeof(B) (это дает член класса B::a), и "a + sizeof(A)" будет указывать не на второй элемент в массиве "a", а куда-то между первым и вторым элементом, так как реальный адрес второго элемента - "a + sizeof(B)". И все бы ничего, но деструктор класс "A" пишет в член "p", тем самым меняя содержимое памяти, а так как для второго элемента адрес вычислен неправильно (его this указывает непонятно куда), то куда реально попадет 0 в присваивании "p = 0;" уже никто не знает, но явно не туда, куда надо. Вот и "Segmentation fault".

Другого объяснения у меня нет.

Если кто знает лучше, поправьте.

P.S. Забавно, что под виндами ничего страшного не происходит.

Update: В комментариях дали точное объяснение из стандарта: C++ 2003 5.3.5:
...In the second alternative (delete array), the value of the operand of delete shall be the pointer value which resulted from a previous array new-expression. If not, the behavior is undefined. [Note: this means that the syntax of the delete-expression must match the type of the object allocated by new, not the syntax of the new-expression.]

Update 2: Объяснение, почему не глючит в Visual Studio.

суббота, 21 февраля 2009 г.

Разница между T() и T

Как совершенно справедливо было замечено в комментариях в посте про разницу между new T() и new T — при объявлении автоматической переменной, а не динамической через new, нельзя использовать скобки, если подразумевается вызвать конструктор по умолчанию. То есть нельзя писать:
T a();
а надо писать:
T a;
так как в первом случае такая запись будет означать декларацию функции a, которая возвращает тип T, а далеко не декларацию переменной класса T с вызовом конструктора по умолчанию.

Не спорю, это очевидно для профессионалов. Для новичков же порой подобная "неочевидная" разница вызывает затруднения, поэтому приведу простейший пример, которые расставит все на свои места.

#include <iostream>
class T {
public:
T() { std::cout << "constructor T()"; }
};

int main() {
std::cout << "T a: ";
// Это синтаксис создания экземпляра класса T с вызовом
// конструктора по умолчанию.
T a;
std::cout << std::endl;

std::cout << "T b(): ";
// А вот это декларация функции "b" без аргументов,
// которая возвращает тип T.
T b();
std::cout << std::endl;
return 0;
}
Данная программа напечатает:
T a: constructor T()
T b():
Видно, что для T b(); никакой конструктор не был вызван. Что в целом и ожидалось.

Использование круглых скобок может быть весьма тонким вопросом в С++.


Другие посты по теме:

суббота, 14 февраля 2009 г.

Тема про параметризированные параметры шаблонов получила продолждение. Один из читателей объяснил, почему пример не компилировался в Visual Studio. Обновленный вариант кода теперь работает во все трех опробованных мной компиляторах: g++, bcc32 и cl.

пятница, 13 февраля 2009 г.

Шаблоны как параметры шаблона

Есть в шаблонах С++ интересная возможность параметризировать сами параметры шаблонов. Чтобы это могло значить?

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

template< typename C, typename E >
void print(const C<E>& v) {
copy(v.begin(), v.end(), ostream_iterator<E>(cout, " "));
cout << endl;
}
И все бы ничего, но с только зрения синтаксиса С++ это неверно. Нельзя просто написать C<E>, если E сам является не определенным типом, а параметром шаблона. Правильный способ использования параметра шаблона, который в свою очередь зависит от другого параметра, должен выглядеть так:
template< template<typename> class C, typename E >
void print(const C<E>& v) {
copy(v.begin(), v.end(), ostream_iterator<E>(cout, " "));
cout << endl;
}
Теперь полный пример (файл template_parameter.cpp):
#include <iostream>
#include <iomanip>
#include <algorithm>
#include <iterator>
#include <string>
#include <vector>
#include <list>
#include <deque>

// Я обычно не использую пространства имен "по умолчанию", но тут
// это сделано для компактности примера.
using namespace std;

// Вся изюминка тут: template<typename> или template<class>.
// Без этого параметр шаблона "С" нельзя будет параметризировать.
// в конструкции C<E>&.
template< template<typename> class C, typename E >
// Тут происходит параметризация параметра "С" параметром "E".
// Без этого класс "С" не может быть использован, так как "E"
// является не просто типом, а тоже параметром шаблона.
void print(const C<E>& v) {
// Так как класс элемента контейнера "Е" нам тут нужен как отдельный
// тип, то для этого и затеяна вся тема с параметризированными
// параметрами шаблона.
copy(v.begin(), v.end(), ostream_iterator<E>(cout, " "));
cout << endl;
}

// Тестовая программа демонстрирует, как одна функция print()
// может использоваться для печати любого контейнера
// (если, конечно, он удовлетворяет требованиям алгоритма
// copy() по наличию должных итераторов), содержащего элементы
// любого типа.
int main(int argc, char* argv[]) {
// Массив целых.
int i[5] = { 1, 2, 3, 4, 5 };
// Создаем вектор, состоящий из целых, и печатаем его.
print< vector, int >( vector<int>(i, i + 5) );

// Массив вещественных.
float f[5] = { .1, .2, .3, .4, .5 };
// Создаем вектор, состоящий из вещественных, и печатаем его.
print< vector, float >( vector<float>(f, f + 5) );

// Массив символов.
char c[5] = { 'a', 'b', 'c', 'd', 'e' };
// Создаем деку, состоящую их символов, и печатаем ее.
print< deque, char >( deque<char>(c, c + 5) );

// Массив строк в стиле С.
char* s[5] = { "a1", "b2", "c3", "d4", "e5" };
// Создаем список, состоящий из строк, и печатаем его.
print< list, string >( list<string>(s, s + 5) );

return 0;
}
Компилируем.

Cygwin:

g++ -o template_parameter_cygwin.exe template_parameter.cpp
или в Borland/Codegear Studio 2007:
bcc32 /etemplate_parameter_cg2007.exe template_parameter.cpp
И запускаем скомпилированный файл:
1  2  3  4  5
0.1 0.2 0.3 0.4 0.5
a b c d e
a1 b2 c3 d4 e5
Отчетливо видно, что на первой строке распечатаны целые, на второй вещественные, на третьей символы, и на четвертой строки.
Вы спросите, где компиляция в Visual Studio? А вот с ней вышел облом. Я пробовал скомпилировать этот пример в Visual Studio 2005 и 2008, и в обоих случаях я получал ошибки типа:
template_as_parameter.cpp(38) : error C3208: 'print' : template parameter list for class template 'std::vector' does not match template parameter list for template template parameter 'C'

Из чего я сделал вывод, что микрософтовский компилятор не поддерживает подобный синтаксис.

Я был очень расстроен подобным фактом, так как в целом очень положительно отношусь к cl.exe. А тут выходит, что даже борландовый компилятор это понимает, а cl.exe нет. Если кто знает, может есть ключик какой секретный для включения поддержки "хитрых и редких" возможностей С++ в компиляторе микрософта — научите, пожалуйста. Буду очень признателен.
Предвосхищу вопросы типа "зачем так сложно, да еще и плохо переносимо" — все верно. Лично я бы отнес все выше описанное к "темным углам" С++, но уж больно интересно по ним полазать.

Обновление
Комментарий Александра прояснил ситуацию с проблемой при компиляции в Visual Studio. Окончательный вариант кода, чтобы работало в cl.exe, таков:
template< template<typename, typename> class C, typename E >
void print(const C<E, allocator<E> >& v) {
copy(v.begin(), v.end(), ostream_iterator<E>(cout, " "));
cout << endl;
}
У шаблонов стандартных контейнеров есть второй параметр, так называемый allocator. Этот параметр часто используется со значением по умолчанию, поэтому редко приходится вспоминать о нем. И как уточнил Александр, моя проблема была в том, что cl.exe требует явного указания наличия этого параметра при параметризации параметра C.

Исправленный код компилируется во всех опробованных компиляторах, теперь включая и cl.exe.


Другие посты по теме:

понедельник, 9 февраля 2009 г.

Темные углы C++

Я нашел таки для себя ответ на вопрос про "лишние скобки" вокруг параметра, задающего интервальный итератор (см. "Скоростное чтение файла в STL через итераторы" ). Например:
std::ifstream is("testfile.txt");
std::string val(
(std::istream_iterator<char>(is)),
std::istream_iterator<char>()
);
Обратите внимание на вроде "излишние" скобки вокруг первого параметра конструктора строки val.
Скотт Мейерс в книге "Эффективное использование STL. Библиотека программиста" в Совете 6 "Остерегайтесь странностей лексического разбора C++" (стр. 42, изд. "Питер" 2002) дает исчерпывающее объяснение этого "феномена". Ответ крайне меня опечалил, так как вскрыл некоторую нелогичность и корявость в целом стройного и красивого языка С++ в данном вопросе. Очевидно, что причины этого в сохранения в С++ обратной совместимости с С, но от этого не легче.

Итак, давайте разберемся по порядку (чтобы меня не обвинили в плагиате, сразу скажу, что я буду следовать примерному тексту Мейерса, так как он дал великолепное объяснение с примерами, и изобретать велосипед в данном случае было бы неразумно). Как мы предполагали, код std::istream_iterator<char>(is) создает экземпляр потокового итератора, привязанного к потоку is. И все бы ничего, если такая конструкция используется как самостоятельное объявление. Вся проблема в именно в использовании такого выражения в контексте вызова функции (в данном случае, конструктора), то есть в качестве параметра. Мейерс приводит следующий пример:

int f(double d);
Это команда объявления функции f, которая получает double и возвращает int.

Тоже самое происходит и в следующей строке. Круглые скобки вокруг имени параметра d не нужны, поэтому компилятор их игнорирует:

int f(double (d));     // То же; круглые скобки вокруг d игнорируются
Теперь третий вариант объявления той же функции. В нем имя параметра просто не указано:
int f(double);         // То же; имя параметра не указано
Три такие формы объявления знакомы всем, хотя про возможность заключения параметра в скобки знают не все (может просто потому, что это очевидно лишние по логике вещей скобки).

Теперь можно рассмотреть еще три объявления функции. В первом объявляется функция g с параметром — указателем на функцию, которая вызывается без параметров и возвращает double:

int g(double (*pf)()); // Функции g передается указатель на функцию
То же самое можно сформулировать иначе. Единственное различие заключается в том, что pf объявляется в синтаксисе без указателей (допустимом как в С, так и в С++):
int g(double pf());    // То же; pf неявно интерпретируется как указатель
Как обычно, имена параметров могут опускаться, поэтому возможен и третий вариант объявления g без указания имени pf:
int g(double());       // То же; имя параметра не указано
Обратите внимание на различия между круглыми скобками вокруг имени параметра (например, параметра d во втором объявлении f) и стоящими отдельно (как в этом примере). Круглые скобки, в которые заключено имя параметра, игнорируются, а стоящие отдельно, обозначают присутствие списка параметров; они сообщают о присутствии параметра, который является указателем на функцию.

Теперь вернемся к оригинальному примеру:

std::ifstream is("testfile.txt");
std::string val(
std::istream_iterator<char>(is),
std::istream_iterator<char>()
);
Сейчас я намеренно убрал таинственные "лишние" скобки вокруг первого параметра.
Что же перед нами тут? Совершенно не то, о чем мы думали изначально. Перед нами объявление функции val, возвращающей тип std::string. Функция получает два параметра:
  • Первый параметр, is, относится к типу istream_iterator<char>. Лишние круглые скобки вокруг is игнорируются.
  • Второй параметр не имеет имени. Он относится к типу указателя на функцию, которая вызывается без параметров и возвращает istream_iterator<char>.
А мы то тут ожидали увидеть описание вызова конструктора, которому передаются два потоковых итератора. Такая интерпретация написанного диктуется одним из основных правил C++: все, что может интерпретироваться как указатель функцию, должно интерпретироваться именно так. Так гласит стандарт:
В грамматике имеется неоднозначность, когда инструкция может быть выражением, так и объявлением. Если выражение с явным преобразованием типов в стиле вызова функции (_expr.type.conv_) является крайним слева, то оно может быть неотличимо от объявления, в котором первый оператор объявления начинается с открытой круглой скобки "(". В этом случае инструкция рассматривается как объявление. — [C++03] п.6.8
Так что же делают эти магические скобки вокруг первого параметра конструктора?
std::ifstream is("testfile.txt");
std::string val(
(std::istream_iterator<char>(is)),
std::istream_iterator<char>()
);
А вот что — объявления формальных параметров не могут заключаться в круглые скобки, я вот заключить в круглые скобки аргумент при вызове функции можно. Вот эти круглые скобки и помогают компилятору решить неоднозначность в нужную нам сторону (а не как положено по стандарту по умолчанию) и точно указать, что перед нами именно использование параметра функции при ее вызове, а не при объявлении.

Соглашусь, от этого может слегка заболеть голова, причем совершенно без причины.

Как написал Герб Саттер в книге "Новые сложные задачи на С++" (он тоже посвятил этому вопросу целую главу, “Задача 23. Инициализация ли это?”, стр. 192, изд. “Вильямс”), что такие моменты синтаксиса С++ являются его "темными углами", и их стоит избегать. Рассмотренный пример можно упростить, объявив итератор отдельно, а не прямо в тексте вызова конструктора, тем самым не заходить в "темный угол". Не так элегантно, зато просто и понятно:

std::ifstream is("testfile.txt");
std::istream_iterator<char> begin(is);
std::istream_iterator<char> end;
std::string val(begin, end);
Читал я недавно, как Линус Торвальдс полоскал С++ за неоправданную языковую сложность. "C++ is a horrible language!", — сказал Линус. Может он и прав.
Мыши плакали, кололись, но продолжали грызть С++.


Другие посты по теме: