Подтвердить что ты не робот

Как работает макрос Q_FOREACH (= foreach) и почему он является сложным?

В Qt существует цикл foreach, который реализуется с использованием макросов (Q_FOREACH). Существуют различные реализации, в зависимости от компилятора.

Определение для GCC выглядит следующим образом:

#define Q_FOREACH(variable, container)                                \
for (QForeachContainer<__typeof__(container)> _container_(container); \
     !_container_.brk && _container_.i != _container_.e;              \
     __extension__  ({ ++_container_.brk; ++_container_.i; }))        \
    for (variable = *_container_.i;; __extension__ ({--_container_.brk; break;}))

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

template <typename T>
class QForeachContainer {
public:
    inline QForeachContainer(const T& t) : c(t), brk(0), i(c.begin()), e(c.end()) { }
    const T c;
    int brk;
    typename T::const_iterator i, e;
};

Контейнер в макросе Q_FOREACH должен быть классом T, который, по крайней мере, должен предоставить тип T::const_iterator, T.begin() и T.end(), как и все контейнеры STL, а также большинство контейнеров Qt, таких как QList, QVector, QMap, QHash,...

Теперь мой вопрос: Как работает этот макрос?

Одна вещь кажется очень странной: переменная появляется только один раз в определении макроса. Так, например, foreach(QString item, list) имеет a QString item =, но no item = после этого в любое время... Как можно изменить переменную item на каждом шаге?

Еще более запутанным является следующее определение Q_FOREACH для компилятора MS VС++:

#define Q_FOREACH(variable,container)                                                         \
if(0){}else                                                                                     \
for (const QForeachContainerBase &_container_ = qForeachContainerNew(container);                \
     qForeachContainer(&_container_, true ? 0 : qForeachPointer(container))->condition();       \
     ++qForeachContainer(&_container_, true ? 0 : qForeachPointer(container))->i)               \
    for (variable = *qForeachContainer(&_container_, true ? 0 : qForeachPointer(container))->i; \
         qForeachContainer(&_container_, true ? 0 : qForeachPointer(container))->brk;           \
         --qForeachContainer(&_container_, true ? 0 : qForeachPointer(container))->brk)

Почему true : 0 ? ...? Разве это не всегда оценивается как 0? Выполняется ли вызов функции qForeachPointer(container), даже если условие до ? истинно?

И зачем нам нужны два for-loops?

Было бы здорово, если бы кто-нибудь мог сделать что-то более понятное для меня!

4b9b3361

Ответ 1

Версия GCC


GCC один очень прост. Прежде всего, он используется следующим образом:

Q_FOREACH(x, cont)
{
    // do stuff
}

И это будет расширен до

for (QForeachContainer<__typeof__(cont)> _container_(cont); !_container_.brk && _container_.i != _container_.e; __extension__  ({ ++_container_.brk; ++_container_.i; }))
    for (x = *_container_.i;; __extension__ ({--_container_.brk; break;}))
    {
        // do stuff
    }

Итак, прежде всего:

for (QForeachContainer<__typeof__(cont)> _container_(cont); !_container_.brk && _container_.i != _container_.e; __extension__  ({ ++_container_.brk; ++_container_.i; }))

Это фактический цикл for. Он устанавливает QForeachContainer, чтобы помочь с итерацией. Переменная brk инициализируется на 0. Затем выполняется условие:

!_container_.brk && _container_.i != _container_.e

brk равно нулю, поэтому !brk истинно, и предположительно, если в контейнере есть какие-либо элементы i (текущий элемент) еще не равен e (последний элемент).

Затем вводится тело этого внешнего for, которое:

for (variable = *_container_.i;; __extension__ ({--_container_.brk; break;}))
{
    // do stuff
}

Итак, x установлен в *_container_.i, который является текущим элементом, в котором итерация включена, и нет никакого условия, поэтому предположительно этот цикл будет продолжаться вечно. Затем вводится тело цикла, которое является нашим кодом, и это просто комментарий, поэтому он ничего не делает.

Затем вводится инкрементная часть внутреннего цикла, что интересно:

__extension__ ({--_container_.brk; break;})

Он уменьшает brk так, что теперь -1 и выходит из цикла (с __extension__, который делает GCC не выдавать предупреждений для использования расширений GCC, как вы знаете).

Затем вводится инкрементная часть внешнего цикла:

__extension__  ({ ++_container_.brk; ++_container_.i; })

который снова увеличивает brk и снова делает его 0, а затем i увеличивается, поэтому мы переходим к следующему элементу. Условие проверено, и поскольку brk теперь 0, а i предположительно еще не равно e (если у нас больше элементов), процесс повторяется.

Почему мы уменьшили и затем увеличиваем brk так? Причина в том, что часть приращения внутреннего цикла не будет выполнена, если мы использовали break в теле нашего кода, например:

Q_FOREACH(x, cont)
{
    break;
}

Тогда brk по-прежнему будет 0, когда он выйдет из внутреннего цикла, а затем будет введена инкрементная часть внешнего цикла и увеличена до 1, тогда !brk будет ложной, а условие внешнего цикла будет оцениваться как false, а foreach остановится.

Хитрость заключается в том, чтобы понять, что существует две петли for; внешнее одно время жизни - это весь foreach, но внутренний - только для одного элемента. Это будет бесконечный цикл, так как он не имеет условия, но он либо break ed из него увеличивает часть, либо break в коде, который вы ему предоставляете. Вот почему x выглядит так, что ему присваивается "только один раз", но на самом деле он назначается на каждой итерации внешнего цикла.

Версия VS


Версия VS немного сложнее, потому что она должна обойти отсутствие расширения GCC __typeof__ и блок-выражений, а версия VS, написанная для (6), не имела auto или другие необычные возможности С++ 11.

Посмотрим на пример расширения для того, что мы использовали ранее:

if(0){}else
    for (const QForeachContainerBase &_container_ = qForeachContainerNew(cont); qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->condition(); ++qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->i)
        for (x = *qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->i; qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->brk; --qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->brk)
        {
            // stuff
        }

if(0){}else заключается в том, что VС++ 6 неверно оценивает переменные for, а переменная, объявленная в части инициализации цикла for, может использоваться вне цикла. Так что это обходной путь для ошибки VS. Причина, по которой они делали if(0){}else вместо просто if(0){...}, заключается в том, что после цикла нельзя добавить else, например

Q_FOREACH(x, cont)
{
    // do stuff
} else {
    // This code is never called
}

Во-вторых, посмотрим на инициализацию внешнего for:

const QForeachContainerBase &_container_ = qForeachContainerNew(cont)

Определение QForeachContainerBase:

struct QForeachContainerBase {};

И определение qForeachContainerNew есть

template <typename T>
inline QForeachContainer<T>
qForeachContainerNew(const T& t) {
    return QForeachContainer<T>(t);
}

И определение QForeachContainer есть

template <typename T>
class QForeachContainer : public QForeachContainerBase {
public:
    inline QForeachContainer(const T& t): c(t), brk(0), i(c.begin()), e(c.end()){};
    const T c;
    mutable int brk;
    mutable typename T::const_iterator i, e;
    inline bool condition() const { return (!brk++ && i != e); }
};

Таким образом, чтобы восполнить недостаток __typeof__ (который аналогичен decltype С++ 11), мы должны использовать полиморфизм. Функция qForeachContainerNew возвращает значение QForeachContainer<T> по значению, но из-за продления времени жизни во времени, если мы сохраним его в const QForeachContainer&, мы можем продлить его срок службы до конец внешнего for (фактически if из-за ошибки VC6). Мы можем хранить QForeachContainer<T> в QForeachContainerBase, потому что первый является подклассом последнего, и мы должны сделать его ссылкой как QForeachContainerBase& вместо значения, подобного QForeachContainerBase, чтобы избежать нарезки.

Тогда для условия внешнего for:

qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->condition(); 

Определение QForeachContainer равно

inline const QForeachContainer<T> *qForeachContainer(const QForeachContainerBase *base, const T *) {
    return static_cast<const QForeachContainer<T> *>(base);
}

И определение qForeachPointer есть

template <typename T>
inline T *qForeachPointer(const T &) {
    return 0;
}

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

У нас есть QForeachContainer<T>, хранящийся в ссылке на QForeachContainerBase, без возможности вернуть его (что мы видим). Мы должны каким-то образом привести его к соответствующему типу и что там, где присутствуют две функции. Но как мы узнаем, к какому типу он должен был его сбрасывать?

Правило тройного оператора x ? y : z заключается в том, что y и z должны быть одного типа. Нам нужно знать тип контейнера, поэтому мы используем функцию qForeachPointer для этого:

qForeachPointer(cont)

Возвращаемый тип qForeachPointer равен T*, поэтому мы используем вывод типа шаблона для вывода типа контейнера.

true ? 0 : qForeachPointer(cont) должен передать указатель NULL правильного типа на QForeachContainer, чтобы он знал, для какого типа следует использовать указатель, который мы ему передаем. Почему мы используем тернарный оператор для этого вместо того, чтобы просто делать qForeachContainer(&_container_, qForeachPointer(cont))? Это позволит избежать оценки cont много раз. Второй (фактически третий) операнд ?: не оценивается, если условие false, и поскольку это условие true, мы можем получить правильный тип cont, не оценивая его.

Итак, это решает это, и мы используем QForeachContainer для перевода _container_ в нужный тип. Вызов:

qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))

И снова определение

inline const QForeachContainer<T> *qForeachContainer(const QForeachContainerBase *base, const T *) {
    return static_cast<const QForeachContainer<T> *>(base);
}

Второй параметр всегда будет NULL, потому что мы делаем true ? 0, который всегда оценивается как 0, и мы используем qForeachPointer для вывода типа T и используем его, чтобы передать первый аргумент в QForeachContainer<T>*, поэтому мы можем использовать его функции-члены/переменные с условием (все еще во внешнем for):

qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->condition()

И condition возвращает:

(!brk++ && i != e)

который совпадает с версией GCC выше, за исключением того, что после его оценки он увеличивает brk. Итак, !brk++ оценивается до true, а затем brk увеличивается на 1.

Затем мы вводим внутренний for и начинаем с инициализации:

x = *qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->i

Который просто устанавливает переменную в значение, на которое указывает итератор i.

Тогда условие:

qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->brk

Так как brk равно 1, вводится тело цикла, что является нашим комментарием:

// stuff

Затем вводится инкремент:

--qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->brk

Это уменьшает brk до 0. Затем условие снова проверяется:

qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->brk

И brk равно 0, которое равно false, и цикл завершен. Мы приходим к инкрементной части внешнего for:

++qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->i

И это увеличивает i на следующий элемент. Тогда мы переходим к условию:

qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->condition()

Что проверяет, что brk равно 0 (это оно) и увеличивает его до 1 снова, и процесс повторяется, если i != e.

Это обрабатывает код break только в том же порядке, что и версия GCC, так как brk не будет уменьшаться, если мы используем break в нашем коде, и все равно будет 1, а condition() будет false для внешнего цикла, а внешний цикл будет break.

И как сказано в комментариях GManNickG, этот макрос очень похож на Boost BOOST_FOREACH, который вы можете прочитать здесь здесь. Итак, у вас есть это, надеюсь, что это поможет вам.