вторник, 26 апреля 2011 г.

воскресенье, 5 декабря 2010 г.

Модуль числа

Неявным преобразованием язык C++ когда-нибудь себе могилу выроет. Сегодня полдня убил на трассировку бага экспорта в формат Collada в 3d-редакторе Blender. Проблема не наблюдается при отсутствии флагов оптимизации. Проблема не наблюдается на 64-битных платформах. Более того, она может исчезнуть при простом вызове printf с выводом переменной.

Дело в том, что арифметика над float и double может значительно различаться как в зависимости от инструкций, так и от процессора. Возьмём, к примеру, следующий код:
#include <iostream>
#include <cmath>
 
void foo(double x, double y)
{
    if (std::cos(x) != std::cos(y)) {
        std::cout << "Huh?!?\n";  // ← вы можете оказаться здесь, даже если x == y!!
    }
}
 
int main()
{
    foo(1.0, 1.0);
    return 0;
}
Точность чисел не обязана здесь быть одинаковой для каждого вычисления косинуса. Если результат вычисления будет перемещён из кеша процессора в оперативную память, станет возможным обрезание значащих битов. Другими словами эту ситуацию можно описать на псевдо-ассемблере:
fp_load x     //занять регистр для работы с числами с плавающей точкой значением переменной x
call _cos     //вызвать cos(double), используя тот же регистр для возврата результата
fp_store tmp  //обрезать результат и разместить его в локальной переменной tmp
 
fp_load y     //занять регистр для работы с числами с плавающей точкой значением переменной y
call _cos     //вызвать cos(double), используя тот же регистр для возврата результата
fp_cmp tmp    //сравнить неусечённый результат в регистре с усечённым значением в tmp

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

Ах да, с чего я начал. В исходнике, который я отлаживал, разработчик вместо функции fabs использовал abs, при этом в качестве параметра был float. Не сразу я вспомнил, что abs работает только с целыми числами.

В большинстве случаев, float-параметр принимал целочисленное значение. Однако даже с целыми значениями из-за описанных выше причин, программа вела себя по разному в разных компиляторах и на разных уровнях оптимизации. Вставка printf в проблемное место магически убирала проблему. Лишь трассировка с gdb навела на эту строчку с abs. Трассировать после -O2-оптимизации столь большого программного комплекса не есть большое удовольствие, честно признаюсь.

Отдельную благодарность выражаю gcc, который даже с параметрами -Wall -Wextra не предупредил о неявной конвертации float в int. Зато теперь я надолго запомнил флаг -Wconversion, включающий уведомления о таких вещах.

вторник, 12 октября 2010 г.

Интерфейсы

Полиморфизм помогает реализовать отношение «является» (is-a) между объектами.

Например, мужчина является человеком, равно и женщина также является человеком. Таким образом, мы имеем:
class Human {
void eat(Object o) {
throw new NotImplementedException();
}
}

class Man extends Human {
private Penis penis;
public void fuck(Woman w) {
w.onFucked(this,null);
}
}

class Woman extends Human {
private Vagina vagina;
public void onFucked(Object o, EventArgs e) {
Man m = (Man)o;
scream(); // Хорошая женщина должна переопределять это поведение.
}
}

Ниже то же самое на UML-диаграмме:


Достаточно очевидно, не правда?

Однако, вскоре на пути встаёт проблема.

Как сейчас видно, между мужчиной и женщиной образовалась зависимость!
Проще говоря, мужчина может заняться сексом исключительно с женщиной.

Такого ограничения нет в реальном мире. Мужчина может заняться сексом со многими объектами, например, с куклой (см. также http://ru.wikipedia.org/wiki/Секс-кукла).

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

Такова суть проблемы.

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

Интерфейс

Интерфейс это набор абстрактных методов.
Если класс реализует (implements) интерфейс, можно гарантировано заявить, что класс обладает всеми методами интерфейса.
Интерфейс ставит ударение на взаимодействии, а не на физических составляющих.
Другими словами, интерфейс заботится о том, что может быть сделано, а не на своём содержимом объектов.

Теперь давайте реализуем интерфейс для всех необходимых классов.
interface IFuckable {
void onFucked(Object o, EventArgs e);
}

Обратите внимание, интерфейс IFuckable не определяет, что будет происходить во время процесса.
Очевидно, женщина является трахаемой. Давайте явно объявим это.
class Woman extends Human implements IFuckable { // обратите внимание на ключевое слово "implements"
private Vagina vagina;
public void onFucked(Object o, EventArgs e) {
Man m = (Man)o;
scream();
}
}

Куклу тоже можно трахнуть, да?

class Toy {}
class Doll extends Toy implements IFuckable {
public void onFucked(Object o, EventArgs e) {
keepSilent();
}
}

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

class Man extends Human {
private Penis penis;
public void fuck(IFuckable fuckableObj) { // Обратите внимание, что fuckableObj не обязан быть женщиной
fuckableObj.onFucked(this,null);
}
}

Как показано на следующей UML-диаграмме:


Теперь всё правильно. Зависимости мужчина-женщина больше нет.



Как ни странно, данная статья была впервые опубликована на китайском форуме. Одно из лучших объяснений отличий между интерфейсом и множественным наследованием в C++, на мой взгляд.

понедельник, 6 сентября 2010 г.

Копирование файла

Итак, в определённый момент вам понадобилось быстро, просто, а самое главное кроссплатформенно скопировать всё содержимое файла. Поскольку речь идёт о C++, вы открываете потоки ifstream (назовём его IN) и ofstream (назовём его OUT):
#include <fstream>

std::ifstream IN("input.txt");
std::ofstream OUT("output.txt");
Вот простейший способ сделать это абсолютно неправильно:
OUT << IN;
Для тех, кто ещё не догадался, почему это не работает, предлагаю создать простой текстовой файл input.txt со следующим предложением:
The quick brown fox jumped over the lazy dog.
Соберите и запустите программу. Результат в output.txt вас определённо удивит.

В данном примере следует учитывать, что классы basic_[io]stream отвечают за форматирование и ни за что иное. Реальным чтением, записью и хранением данных занимается семейство basic_streambuf. К счастью, существует operator<< (принимающий ostream и указатель на streambuf), чтобы помочь в ситуации простого копирования.

Почему указатель на streambuf, а не просто streambuf? Дело в том, что потоки [io]stream содержат указатели (или ссылки, в зависимости от реализации), а не сами буферы. Это позволяет реализовать полиморфическое поведение как для буферов, так и для самих потоков. Указатель легко получить с помощью метода rdbuf(). Поэтому простейшим способом копирования файла является
OUT << IN.rdbuf();
Так что же происходит в случае OUT<<IN? UB, неопределённое поведение, поскольку для данных типов operator<< не определён. Возможны реализации, в которых он определён, но даже в этом случае будут потеряны все пробельные символы и в итоге вы увидите фразу "Thequickbrownfox...". Если в реализации оператор не определён, то скорее всего IN будет преобразован в void*, и в выводе вы увидите шестнадцатеричное представление адреса. Некоторые компиляторы могут просто отказаться компилировать такой код.

В заключение замечу, что всё вышесказанное не относится o*f*streams, поскольку у них в родительском классе определён operator<<.

О размере контейнера std::list

Много людей считают, что для стандартных контейнеров вычисление числа элементов должно выполняться за константное время. Однако немногие знают, что такое поведение не относится к std:list:size(). Сложность std:list:size() в реализации STL от SGI (используемой, например, в gcc), оценивается как O(n). Вот, что говорится об этом в пояснении SGI:
Фунция size() классов list и slist отрабатывает за время, пропорциональное числу элементов в списке. Это преднамеренный компромисс. Единственный способ получить размер связанных списков — предоставлять специальное поле внутри контейнера. Это потребует дополнительного времени для обновления этого поля (так, например, это приведёт к тому, что операция splice() будет выполняться за линейное время), а также сделает контейнер больше. Многие алгоритмы для связных списков не требуют этого дополнительного поля (для алгоритмов, требующих его, возможно, выгоднее работать с контейнером vector), а если необходимо сохранять размер списка, то пользователи в состоянии делать это сами.
Выбранный путь не противоречит стандарту C++. Стандарт утверждает, что size() «стоит» (should) отрабатывать за константное время, а «стоит» не есть то же самое, что «обязан» (shall). Согласно официальной терминологии стандартов ISO это подразумевает, что в данное реализации предполагается в случае, если нет видимых причин для отказа.

Вывод: никогда не пишите
if (L.size() == 0)
    ...

Вместо этого пишите:
if (L.empty())
    ...