Стандартный пустой тип аргумента работает как void - PullRequest
0 голосов
/ 01 мая 2020

Есть ли в стандарте тип, который работает как void? то есть что-то, что на самом деле пропущено в списке аргументов, например структура с тегами?

void foo(){}
void foo(std::empty_type){}
// assembly for these two should be the same, no arguments

Мой пример использования: я пишу свой собственный объект-итератор на основе диапазона for. Проблема в том, что возвращаемого объекта достаточно для проверки, находится ли он в конце (я оборачиваю API). Этот for, основанный на диапазоне, сильно пострадал, и любые незначительные оптимизации приведут к увеличению производительности кода c. Дело в том, что я не могу объявить унарный operator != и не могу объявить символ типа void в соответствии со стандартом, поэтому я хотел бы эмулировать его с объектом размера 0.

Что тип я могу использовать, чтобы намекнуть компилятору вообще пропустить аргумент? Будет ли работать что-то вроде std::monostate? Из моих тестов на MSV C, using empty_type = struct {} не является решением.

Ответы [ 2 ]

6 голосов
/ 01 мая 2020

Вот для чего нужны часовые.

Я предполагаю, что ваш итератор выглядит примерно так:

struct iterator {
    // ... usual iterator interface ...

    // iterator knows when it's done
    bool is_done() const;
};

Вы можете сделать свой собственный тип стража:

struct is_done_sentinel { };

Добавьте сравнение между ними (которое в C ++ 20 является просто одним оператором, в C ++ 17 вы должны написать все четыре):

bool operator==(iterator const& lhs, is_done_sentinel ) {
    return lhs.is_done(); // adjust as appropriate for your actual iterator
}

А затем ваш диапазон вернет это sentinel как его конец:

struct range {
    iterator begin();
    is_done_sentinel end() { return {}; }
};

Это будет иметь поведение, которое вы хотите: ваш диапазон заканчивается, когда итератор завершен, и нет никаких накладных расходов на проверку.

2 голосов
/ 01 мая 2020

Проблема в том, что возвращаемого объекта достаточно для проверки, находится ли он в конце (я оборачиваю API).

C ++ 20 распознает это как "Iterator / Диапазон Sentinel, который заменил традиционную модель «Итератор / Итератор», которая требует, чтобы два Итератора были одного типа. C ++ 17 был на самом деле преимущественно расширен для поддержки такой конструкции, как C ++ 14 и раньше требовал, чтобы итераторы begin и end основанного на диапазоне типа диапазона for l oop быть того же типа. C ++ 17 изменил это, чтобы учесть разные типы.

Sentinel - это тип, который действует как конечный итератор, не будучи итератором. Действительно, единственное реальное требование к Sentinel - это то, что он проверяется на равенство с итератором, с которым он связан. Любой итератор, который проверяет равенство своего стража, считается концом диапазона.

Очевидно, что другим итератором является Страж. Но тот факт, что Страж не должен быть Итератором, расширяет возможности.

Если у вас есть значение Стража, которое не является Итератором, вероятно, что Страж ничего не делает. В большинстве таких случаев итератор хранит всю информацию, необходимую для определения того, находится ли она в конце. istream_iterator и подобные потоковые итераторы являются примерами такого рода, и они могут иметь Sentinels (они не имеют, поскольку они предшествуют парадигме Sentinel. Вместо этого «конечный» итератор для такого диапазона является построенным по умолчанию итератором) , Таким образом, ваш operator== - это просто способ вызова Iterator.IsEnd() или любой другой функции, которая вам нужна.

Какой тип я могу использовать, чтобы подсказать компилятору вообще пропустить аргумент?

Нет. Но это нормально, потому что вам не нужно.

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

Это означает, что тип диапазона не должен хранить Sentinel; просто нужно, чтобы end произвел один. Таким образом, он возвращает значение только что созданного Sentinel.

Поскольку (завершенные) объекты в C ++ должны занимать память, он будет иметь размер от 1 до 4 байтов. В худшем случае в стеке находится 4-байтовый объект.

Далее range- for вызовет operator== в паре Iterator / Sentinel. Ну, в принципе, нет оправдания тому, что эта функция не была сделана inline, и, поскольку все, что эта функция будет делать, это вызывать функцию в Итераторе, у компилятора (с включенной оптимизацией) нет оправдания, чтобы не встроить функцию.

И как только он встроен ... компилятор видит, что Страж ничего не делает. Поскольку функция встроенная, она не будет передана в качестве параметра. И это на самом деле не используется встроенным кодом; он просто сидит там.

В худшем случае, компилятор оставит в стеке 4-байтовый объект, который ничего не делает. Но, скорее всего, компилятор просто избавится от него.

Но если вы не хотите поверить мне на слово, я представляю вам nul_terminator, Sentinel для const char* итераторов, которые указывают на NUL-концевые строки. Естественно, operator== разыменовывает итератор и проверяет его на 0.

struct nul_terminator {};

//This code is C++17, since C++20 support is spotty, but you only need one of these in C++20.
bool operator==(const char *it, nul_terminator) {return *it == 0;}
bool operator==(nul_terminator, const char *it) {return *it == 0;}
bool operator!=(const char *it, nul_terminator) {return *it != 0;}
bool operator!=(nul_terminator, const char *it) {return *it != 0;}

class string_range
{
public:
    const char *begin() {return str_;}
    nul_terminator end() {return {};}

    string_range(const char *str) : str_(str) {}

private:
    const char *str_;
};

int manual_sum_values_of_string(const char *str)
{
    int accum = 0; //Just to give the function something to do, so the optimizer doesn't make it go away.
    for(; *str != 0; ++str)
        accum += *str;

    return accum;
}
int range_sum_values_of_string(string_range str)
{
    int accum = 0; //Just to give the function something to do, so the optimizer doesn't make it go away.
    for(auto ch : str)
        accum += ch;
    return accum;
}

Если вы посмотрите на выходные данные сборки из трех основных компиляторов для manual_sum_values_of_string против range_sum_values_of_string вы обнаружите, что они практически идентичны. И я не имею в виду «есть накладные расходы, но вы можете игнорировать это». Я имею в виду «то же самое, за исключением того, что порядок пары независимых кодов операций поменяется местами».

G CC дает эквивалентные результаты на уровне оптимизации 1, в то время как Clang и MSV C требуют O2, прежде чем они получить равные результаты.

В принципе: не беспокойтесь об этом. Компиляторы умны, так что пусть они делают свою работу.

...