Как стать автором
Обновить

Комментарии 60

НЛО прилетело и опубликовало эту надпись здесь
Я сперва подумал, что её автор — это вы. Потом не увидел букв R в примерах кода…
НЛО прилетело и опубликовало эту надпись здесь
В пункте про побочные эффекты вы ещё забыли упомянуть частую проблему — макросы могут вычислять свои аргументы несколько раз. В худшем случае это приводит к странным побочным эффектам, в более легком — к проблемам производительности.

Пример
#define SQR(x) ((x) * (x))

y = SQR(x++);
Вообще, если мне не изменяет память, x++ внутри аргумента функции может привести к UB, так что лучше отучить себя вообще запихивать инкремент в аргументы, массивы и т. п.

Это частая проблема и тут даже не в макросах дело. Особенно не могут не радовать конструкции вида:

a[i++] = i;

Или вот пример из уважаемой библиотеки glibc:

 while (n >= 0)
   if (groups[n--] == gid)

Не надо так.
foo(x++, x++), емнип, unspecified behavior, а не undefined, а вот пример выше таки undefined
Я ошибся, foo(x++, x++) — таки undefined, конечно. Unspecified: foo(inc(x), inc(x))
Не вижу проблем в коде из glibc?
Я этот баг искал два дня не меньше, выражалось это в том, что у пользователя всегда оставалась группа рута (подробности не помню, боюсь соврать). На первый взгляд проблем нет, но по факту оказывалось следующее: из-за того что внутри массива постфиксный декремент, из-за чего границы массива оказались не те, которые ожидал программист. Подробнее: sourceware.org/bugzilla/show_bug.cgi?id=11701

Вот наглядно, к чему это приводит:

#include <stdio.h>
int main(int argc, char **argv)
{
	int i;
	int n = 10;
	int groups[n];
	for(i = 0; i < n; i++)
	{
		groups[i] = i;
	}
	while (n >= 0)
		printf("%d\n", groups[n--]);
}
$ ./test
4195728
9
8
7
6
5
4
3
2
1
0

Здесь не UB, просто излишне «хакерский» стиль программирования привёл к тому, что никто, включая автора, не понимает, как этот цикл работает. Согласитесь, это опасный стиль программирования для кода, который работает с правами доступа. Хорошо, это не привело к уязвимости — в ядре код проверки на группы более адекватный и не содержит таких ошибок. Пользователь через функцию access получал утвердительный результат, но когда пытался обратиться к файле через системный вызов open — получал уже от ворот поворот.
Не вижу ничего особенно опасного в этом стиле, это ж просто off-by-one error.

Такой вариант

    while (n)
        printf("%d\n", groups[--n]);


или даже такой вариант

    while (n --> 0)
        printf("%d\n", groups[n]);


ничуть не более «хакерский».
Ну если перспектива вылететь за границы массива вас не пугает — то да.

Напомню, что конкретно делает эту ошибку «роковой» с точки зрения защиты:

1. Цикл начинается с конца массива groups.
2. Из-за ошибки программиста первая итерация цикла начинается за границей массива groups.
3. По счастливой случайности там оказывается ноль. Да, это работает «корректно» до тех пор, пока на стеке мусор, но ведь всякое бывает.
4. Ноль — это ID рута.
5. Так как функция проверяет на принадлежность пользователя определённой группе, из-за этой ошибки пользователь получает группу рута.

glibc и так в общем-то не подарок, а мне приходится потом искать причину глюков с собаками дебаггером.

На мой взгляд баг возник не из-за невнимательности, а именно из-за привычки программистов вкрючивать инкремент в самые неожиданные места. Не может не радовать, что тенденция писать в таком стиле постепенно сходит на нет.
foo(i++) — вполне легальная конструкция, если foo — это метод или функция.

Зато нелегально foo(i++, i++), так как здесь i изменяется дважды до точки следования (я, правда, не уверен, не изменились ли как-то точки следования согласно C++11).

А макрос плох тем, что «скрывает» это, маскируясь под функцию с параметром.
Это вообще опасный поворот: дело в том, что запятая в роли оператора запятая в C++ является точкой следования, а запятая в роли разделителя аргументов функции — точкой следования не является. Если автор кода плавает в стандарте — эти грабли он поймает, рассуждая так:
-Дык запятая же является точкой следования!
Является. Пока исполняет роль оператора. Но как только она становится разделителем аргументов — увы, она перестает им быть.

И тут на сцене появляется библиотека Eigen, со своим лихим способом задания матриц:
m << 1, 2, 3,
4, 5, 6,
7, 8, 9;

Здесь запятая в роли оператора (а значит, является точкой следования), так что можно творить инкременты.
Здесь запятая в роли оператора (а значит, является точкой следования)
Перегруженный оператор запятая (как и операторы || и &&) теряет свои специальные свойства, превращаясь в простой вызов функции. Поэтому, скажем, такой код содержит UB (gcc даже выдает об этом предупреждение):
struct C {};
C& operator,(C& c, int) { return c; }

C c;
int i = 0;
c, i++, i++;

поскольку эквивалентен следующему коду:
f( f(c, i++), i++);
Вот уж действительно, век живи — век учись. Так как я не пользуюсь запятой, не учитывал этой тонкости. И держал eigen как единственный оправданный пример использования запятой-оператора.
Спасибо.
Спасибо, забыл о подобном кейсе, добавил Ваш пример в статью.
Я на тёмной стороне, я пишу на Си.
Да, в комментариях возможно все!
Для меня khash до сих пор является непревзойдённым контрпримером использования макросов:)
Boost.Preprocessor для себя откройте:) Я открыл (немножко)… оказалось что на макросах можно делать ТАКИЕ вещи… фактически можно вводить новые языковые возможности. Я даже не понимаю как оно работает… а оно работает. Причем это даже не С++, в чистом Си такое тоже будет работать. Говорят еще в libCello тоже на макросах сделали фактически новый язык, хотя формально это всего лишь библиотека на Си.
Можно ли libCello использовать на практике? У меня сложилось впечатление, что программа, написанная на Python, будет быстрее, чем написанная на Си+libCello. Интересный проект, но не более того.
Вариант с безопасным вызовом функции можно заменить на версию с variadic templates.
А не поделитесь? Я попробовал, у меня получился страшный Франкенштейн.
Хотя, не так уж и страшно, типы автоматически выводятся… Вы вот такое имели в виду?
template<typename ReturnType, class Class, typename... ParameterTypes>
static inline ReturnType safeCall(const ReturnType& defaultValue, Class *ptr, ReturnType (Class::*Method)(ParameterTypes...), ParameterTypes... arguments)
{
    if(ptr)
        return (ptr->*Method)(std::forward<ParameterTypes>(arguments)...);

    return defaultValue;
}

// вызов
auto x = safeCall(defaultRetval, pointer, &Class::method, param);
А вот появятся в след. стандарте «extention methods» и возможно будет еще проще. Хотя смотря какие в итоге ограничения будут.
«extention methods» давно напрашивались, учитывая изначальные идеи STL (внешние функции для работы с объектами, отсутствие виртуальных деструкторов). Но вот что с их помощью будет можно решить эту задачку, я все же сомневаюсь. Но, конечно, будем посмотреть.
Семантика будет немного приятнее.
Ага, примерно это. Можно их еще и асинхронно вызывать или наоборот делать асинхронные методы синхронными. Простор огромный, да и всяко безопаснее и удобнее макросов.
В целом нормальная статья.

Все же практика показывает, что если без макроса можно обойтись, то без него нужно обойтись. По этой причине ценность макросов типа FOREVER, категорически сомнительна. Что такое while(true) знают все, а что такое FOREVER еще надо разбираться. Может там ракеты в космос запускают. Одно дело, когда макрос заменяет большую портянку и совсем другое, когда одно короткое слово заменяется на другое.

«Безопасный вызов» — это опять же очень спорный момент.
if (x) x->foo();
Ну и вполне себе нормально. Не слишком много лишнего. Вот если в коде таких вызовов много — может с кодом что-то не то?

Для меня ценными являются такие макросы (к вопросу про «превращение в строку»)
#define FOO(X) foo(#X, X)

Часто полезно при перечислении полей объекта для сериализации. Чтобы не ошибиться.

Не перечислены макросы, которые работают для старого и нового стандарта:
OVERRIDE, BOOST_AUTO, BOOST_FOREACH
Впрочем эти надеюсь скоро полностью уйдут в прошлое.
Насчете forever, думаю, это больше вопрос вкуса. Все же ничего особо страшного этот макрос не делает (более того, делает ровно то, то написано).

Про «безопасный вызов» — меня уже ткнули носом в реальную проблему. Касательно спорности, я нахожусь под впечатлением идеи Null propagating operator для C#.
Дополню, я тоже использую макросы для двух вещей
1. Генерация символьных констант
2. генерация имен, через ##, например (сериализация variant-like:):
#define READ_CASE(type) case TYPE_##: dataStream >> _data.f_##type; break

struct variant {
   union {
    int8_t f_int8_t;
    uint8_t f_int8_t;
   // ...
   } _data;
   _type;
}
//..
dataStream >> _type;
switch(_type) {
READ_CASE(bool);
READ_CASE(int8_t);
READ_CASE(uint8_t);
READ_CASE(int16_t);
//..
}



Premature pessimization на пустом месте. findObject() будет вызываться дважды в случае ненулевого объекта:

prefix_safeCallVoid(findObject(), method());

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

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

Здесь, лишний раз аллоцируется память
prefix_safeCallVoid( std::make_unique<T>(), foo );


А это не скомпилируется со странной диагностикой
prefix_safeCallVoid( std::make_unique<T>(1,2,3), foo );


Здесь memory-leak
prefix_safeCallVoid( createObject(), add_to_list(pool) );


Спасибо за подробный разбор. Я уже все осознал и исправился.
К сожалению (конечно же, для меня, а не для проектов), при его использовании подобных проблем у меня не возникало.
Если уж создавать подобный макрос, то нужно гарантировать, что аргументы гарантированно используются ровно 1 раз

Благодаря lemelisk удалось решить проблему при помощи лямбд. Теперь макрос подставляет лямбду, и «передает» в нее параметры. Собственно, получилось как раз то, о чем я сам же и писал в статье: «логику макросов перенести в функции, а сами макросы сделать ответственными только за передачу данных в эти функции».

или что лучше — написать шаблонный метод.

У шаблонного метода мне не нравится то, что нужно передавать указатель на метод.
У шаблонного метода мне не нравится то, что нужно передавать указатель на метод.
Минусом работы через указатель на метод является то, что отваливаются аргументы по умолчанию для данного метода. Этого можно избежать, передавая вместо указателя функциональный объект вида:
[&](auto&& ptr){ return ptr->some_func(...); }
но синтаксис будет уже слегка многословным.
Я немного подумал и скрыл этот макрос в статье. Вы полностью правы.
mult всё-таки должен выглядеть так: ((a)*(b)), а то не поделить на него будет.
Я не подразумевал демонстрировать там идеальный макрос умножения. Но, на всякий случай, поправил. Спасибо.
Ещё не смотрел написано ли про это по вашим ссылкам, но лямбда-функции в С++11 вместо возни с конструкцией do { ... } while (false) (которая ещё и не даёт вернуть из неё значение) позволяют развернуть макрос в определение функции и её последующий вызов с нужными аргументами:
#define safeCall(defaultValue, objectPointer, methodWithArguments) \
    [](auto&& ptr){ \
        return ptr ? (ptr->methodWithArguments) : (defaultValue); \
    } /* define function */ \
    (objectPointer) /* and call */

struct Test {
    int test(int value) { return value; }
};

int main()
{
    Test* ptr1 = nullptr;
    Test* ptr2 = new Test;
    cout << safeCall(0, ptr1, test(10)) << ' ' << safeCall(0, ptr2, test(10));
}
Интересная штука. У Страуструпа в статье много упоминаний про замену макросов на лямбды, но вот такого симбиоза там вроде нет.
Пришлось немного переделать, чтобы заработало у меня в XCode:
#define safeCall(defaultValue, objectPointer, methodWithArguments)\
    [&](const decltype(defaultValue)& defaultRetval, const decltype(objectPointer) pointer)\
    {\
        return pointer ? (pointer->methodWithArguments) : defaultRetval;\
    }\
    (defaultValue, objectPointer)

struct Test {
    int test(int value) { return value; }
};

int main(int argc, const char* argv[])
{
    Test* ptr1 = nullptr;
    Test* ptr2 = new Test;
    auto def = 0;
    int param = 20;
    if(0 == safeCall(def, ptr1, test(param)))
        std::cout << safeCall(0, ptr2, test(param)) << std::endl;

    return 0;
}
Вызов лучше вот так:
((defaultValue), (objectPointer))
И void версия:
#define safeCallVoid(objectPointer, methodWithArguments)\
    [&](const decltype(objectPointer) pointer)\
    {\
        if(pointer)\
            (pointer->methodWithArguments);\
    }\
    (objectPointer)
Лямбды с auto в списке параметров разрешены только в С++14 (компилировать надо с опцией -std=c++1y). В вашем коде несколько косяков:
  • [&] — не нужен, т.к. вы ничего не захватываете на самом деле.

  • const decltype(defaultValue)& — сразу несколько проблем. Самое очевидное — значение по умолчанию вычисляется в любом случае, независимо от того нулевой указатель или нет. Во-вторых, оно насильно сделано const lvalue, поэтому если у типа, который мы возвращаем, нету конструктора копирования (а есть только перемещения), то ваш код не компилируется:
    struct Fail {
        unique_ptr<int> get_ptr() { return unique_ptr<int>(new int); }
    };
    
    safeCall(unique_ptr<int>{}, new Fail, get_ptr());
    

    Вообще, тут надо хорошенько всё продумать (что именно мы хотим эмулировать, всегда ли хотим возвращать объект по значению, проследить, чтобы сохранялись value categories, и так далее), и, скорей всего, тернарный оператор тут плох, надо писать if и два return.

  • const decltype(objectPointer) pointer — в некоторых случаях эта конструкция развернется в приём указателя по значению. В случае встроенных указателей ничего страшного в этом нет, но smart pointer'ы работать перестанут:
    unique_ptr<Test> ptr;
    safeCall(0, ptr, test(10));
    


Будут вопросы, пишите лучше в личку, а то зафлудим тут всё.
Написал в личку, хоть и не совсем вопросы. Насчет [&] — Вы не правы.
Добавил в статью. Спасибо.
safe_call должен иметь ленивую семантику как по имени и аргументам метода, так и по подстановке дефолтного значения. Поэтому энергично вычисляемый указатель на объект правильно вынесен из лямбды, а вот дефолт нужно записать внутрь.
А с C++14 есть же Lambda captures expressions:
#define prefix_safeCallVoid(objectPointer, methodWithArguments)\
[&, pointer = objectPointer]()\
{\
    if(pointer)\
        (pointer->methodWithArguments);\
}\
()
Захват указателя по значению работает в случае обычных указателей, но не будет работать для smartpointer'ов. Нужен именно захват по auto&&.
Жаль, что вывод типа сделан такой же, как и для auto. Но и auto&& нельзя особо сделать — появляется проблема со ссылками, в случае инициализации с помощью l-value. Мда…
Но и auto&& нельзя особо сделать — появляется проблема со ссылками, в случае инициализации с помощью l-value
ИМХО, тут все ок
Есть синтаксис захвата по ссылке, но только по lvalue:
[&ref = ...]

Невозможность захвата по rvalue ссылке, на мой взгляд, сделана специально, потому что я практически уверен, что такая ссылка не продляет время жизни временного объекта (по аналогии с ссылкой-членом класса). По крайней мере результат выполнения такого кода на clang (у gcc 4.9.0 какой-то баг с захватом по константной ссылке, он не хочет даже const lvalue по ней захватить):
int main()
{
    struct T {
        T() { cout << "Begin lifetime\n"; }
        ~T() { cout << "End lifetime\n"; };
    };
    using CT = const T;
    auto f = [&x = CT{}, y = CT{}](){};
    cout << "----\n";
}
показывает, что временный объект даже до конца выражения не доживает.
Спасибо за статью!

Обычно не использую макросы в своих программах на C++, но есть один, уж очень удобный при отладке:

#define PRINT(A)\
std::cout << #A << " = [" << (A) << "]\n";

Выводит имя и значение переменной (или выражения).
Вот еще полезный макрос для сообщений об ошибках с отладкой

inline std::string location(const std::string& file, int line) {
	std::ostringstream oss;
	oss << file << ": " << line;
	return oss.str();
}

#define MY_LOCATION location(__FILE__, __LINE__)
Размер массива (arraysize):
// The arraysize(arr) macro returns the # of elements in an array arr.
// The expression is a compile-time constant, and therefore can be
// used in defining new arrays, for example.  If you use arraysize on
// a pointer by mistake, you will get a compile-time error.
//
// One caveat is that arraysize() doesn't accept any array of an
// anonymous type or a type defined inside a function.  In these rare
// cases, you have to use the unsafe ARRAYSIZE() macro below.  This is
// due to a limitation in C++'s template system.  The limitation might
// eventually be removed, but it hasn't happened yet.

// This template function declaration is used in defining arraysize.
// Note that the function doesn't need an implementation, as we only
// use its type.
template <typename T, size_t N>
char (&ArraySizeHelper(T (&array)[N]))[N];

// That gcc wants both of these prototypes seems mysterious. VC, for
// its part, can't decide which to use (another mystery). Matching of
// template overloads: the final frontier.
#ifndef _MSC_VER
template <typename T, size_t N>
char (&ArraySizeHelper(const T (&array)[N]))[N];
#endif

#define arraysize(array) (sizeof(ArraySizeHelper(array)))
Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории