Маша С++
104 subscribers
54 photos
2 files
65 links
Учу C++ и не только вместе с вами, мои котятки)
Download Telegram
Type traits часть 1 (перевод)

Type traits - это фича C++, которая позволяет выполнить compile-time анализ типов в нашем коде.

Использование type traits

Стандартная библиотека C++ включает большую коллекцию type traits и связанных функций в заголовке <type_traits>:

#include <type_traits>


Почти все type traits стандартной библиотеки являются шаблонизированными структурами. Они принимают тип для анализа как параметр шаблона и предоставляют результат анализа через статическое поле value.

Например, std::is_arithmetic сообщает, является ли тип числовым:

std::is_arithmetic<int>::value // true
std::is_arithmetic<std::string>::value // false


Мы можем использовать эти булевы значения как обычно в нашем коде:

#include <type_traits>
#include <iostream>
#include <vector>

int main() {
if (std::is_arithmetic<float>::value) {
std::cout << "float is arithmetic";
}

if (!std::is_arithmetic<std::string>::value) {
std::cout << " but std::string isn't";
}

using vector_trait =
std::is_arithmetic<std::vector<int>>;

if (!vector_trait::value) {
std::cout
<< "\nNeither is std::vector<int>";
}
}
// Output:
// float is arithmetic but std::string isn't
// Neither is std::vector<int>


Начиная с C++17, есть сокращенный способ получения value: добавить _v к имени класса:

#include <type_traits>
#include <iostream>

int main() {
if (std::is_arithmetic_v<double>) {
std::cout << "double is arithmetic\n";
}
return 0;
}


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

#include <type_traits>
#include <iostream>

template <typename T>
void Function(T Param) {
if (std::is_arithmetic<T>::value) {
std::cout << "Type is arithmetic\n";
} else {
std::cout << "Type is not arithmetic\n";
}
}

int main() {
Function(100);
Function("Hello World");
return 0;
}
// Output:
// Type is arithmetic
// Type is not arithmetic
3👍1🔥1
Type traits часть 2 (перевод)

Compile-type условия с if constexpr

Напоминаем, что type trait вычисляется во время компиляции, а не выполнения. Когда мы хотим выполнить проверку чего-то, известного во время компиляции, то мы можем добавить constexpr к базовому if (нужен C++17):
#include <type_traits>
#include <iostream>

template <typename T>
void Function(T Param) {
if constexpr (std::is_integral_v<T>) {
std::cout << "Integer!\n";
}

if constexpr (std::is_floating_point_v<T>) {
std::cout << "Floating point number!\n";
}
}

int main() {
Function(42);
Function(3.14);
return 0;
}
// Output:
// Integer!
// Floating point number!


Проверка if во время компиляции позволяет полностью удалить весь неподходящий код. Можно представить, что остается такой код:
void Function(int Param) {
std::cout << "Integer!\n";
}

void Function(double Param) {
std::cout << "Floating point number!\n";
}


Рассмотрим еще пример с другими трейтами:
#include <type_traits>
#include <iostream>

class Actor {};
class Monster : Actor {};

template <typename T>
void Function(T Param) {
using namespace std;
if constexpr (is_pointer_v<T>) {
cout << "\nType is a pointer";
}

if constexpr (is_class_v<T>) {
cout << "\nType is a class";
}

if constexpr (is_same_v<Actor, T>) {
cout << ": Actor";
} else if constexpr (is_base_of_v<Actor, T>) {
cout << ": Actor (or derived from Actor)";
}
}

int main() {
int x{5};
Function(&x);

Actor MyActor;
Function(MyActor);

Monster MyMonster;
Function(MyMonster);
return 0;
}
// Output:
// Type is a pointer
// Type is a class: Actor
// Type is a class: Actor (or derived from Actor)


Больше трейтов описано тут.
👍4🔥2
Type traits часть 3 (перевод)

Рассмотрим такую простую шаблонную функцию:
template <typename TFirst, typename TSecond>
auto Average(TFirst x, TSecond y) {
return (x + y) / 2;
}

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

Однако такой код позволяет слишком многое. Хотя наша функция предназначения для работы с числами, это явно нигде не обозначено. Что если передвать в нее не числовой тип? Есть два варианта развития событий.

В лучшем случае, используемый тип не будет поддерживать один из операторов, например /. Это приведет к ошибке компиляции, хотя и не очень понятной:
binary '/':
'std::basic_string<char,std::char_traits<char>,std::allocator<char>>'
does not define this operator or a conversion to a type acceptable
to the predefined operator

В худжем случае, переданный тип реализует все требуемые операторы, но их поведение не такое же, как предполагает наша функция (а именно арифметическое сложение и деление для + и /). Ведь типы свободны перегружать операторы + и / для любой цели, они не обязаны означать сложение и деление.
В таком случае, компилятор с радостью создаст экземпляр шаблона, так как тип соответвует всем требованиям. И мы не получим ошибку компиляции, хотя получили баг.

Type traits позволяют нам быть более явными относительно требований. Самый базовый пример - это просто использовать static_assert:
#include <type_traits>

template <typename TFirst, typename TSecond>
auto Average(TFirst x, TSecond y) {
static_assert(
std::is_arithmetic<TFirst>::value,
"Average: First argument must be arithmetic");
static_assert(
std::is_arithmetic<TSecond>::value,
"Average: Second argument must be arithmetic");
return (x + y) / 2;
}

Это имеет три преимущества:
- реализация требований. Теперь компилятор будет проверять, что предоставленный тип это число, а не просто что он реализует операторы + и /
- документация требований. Любой, кто взглянет на нашу функцию, увидит какие есть требования. Некоторые IDE смогут сразу предоставить обратную связь до компиляции.
- более полезные сообщения об ошибках, если тип не соответвует требованиям:
int main() {
Average("Hello", "World");
}

static_assert failed: 'Average: First argument must be arithmetic'
static_assert failed: 'Average: Second argument must be arithmetic'
👍5
Задачка для собеса. Пусть дан вектор беззнаковых целых, в которых есть нули. Нужно сдвинуть все нули в конец вектора, то есть написать код функции, используя стандартные алгоритмы:
#include <iostream>
#include <vector>
#include <algorithm>

void move_zeroes_to_end(std::vector<size_t>& v)
{
// TODO
}

int main()
{
std::vector<size_t> v = { 0, 4, 8, 3, 7, 0, 1, 4, 4, 0 };

move_zeroes_to_end(v);

for (auto i : v) {
std::cout << i << " ";
}
std::cout << std::endl;

return 0;
}


Конечно, можно написать код самому, но так вы не покажете знания std. Как вариант, можно сделать сортировку в обратном порядке:
std::sort(v.rbegin(), v.rend());
но тогда порядок нарушится и плюс это занимает O(N·log(N)).

Другим более интересным вариантом будет использование std::remove:
    auto zero_it = std::remove(v.begin(), v.end(), 0);
std::fill(zero_it, v.end(), 0);

Дело в том что remove (в отличие от std::erase) не удаляет физически элементы из контейнера (про это уже было писано на канале). Вместо этого, нужные элементы сдвигаются в начало контейнера, а в конце остается "мусор". Нам остается только забить нулями "мусор", начало которого remove и вернет.
👍31
cpp_books_roadmap.png
951.3 KB
Роадмап для изучения C++ (и не только) из книжек. Взято тут.
🔥8🤔1
Оператор присвоения копии получает аргумент того же типа, что и класс.
Для совместимости с оператором присвоения встроенных типов операторы присвоения обычно возвращают (неконстантную) ссылку на свой левый операнд:
#include <iostream>

class Data
{
public:

Data(int i = 0) : _i(i) {}

Data& operator=(const Data & other) // copy assignment
{
std::cout << "operator= " << _i << std::endl;
_i = other._i;
return *this;
}

int i() const { return _i; }

private:
int _i;
};

int main() {

Data d1(1);
Data d2(2);
Data d3(3);

d1 = d2 = d3;

std::cout << d1.i() << d2.i() << d3.i() << std::endl;

return 0;
}

// Output:
// operator= 2
// operator= 1
// 333

Как видим, благодаря возврату ссылки, работает множественное присваивание: d1 = d2 = d3;.

Однако, оператор присвоения копии может возвращать любое значение.
Если мы захотим возвращать ничего, то это тоже возможно:
void operator=(const Data & other)
В этом случае, множественное присваивание не сработает, однако единичное вполне:
    d1 = d2 = d3; // ошибка!
d1 = d2; // ок


Мы также можем (например по ошибке) вернуть не ссылку, а сам объект, а также например строку:
#include <iostream>
#include <string>

class Data
{
public:

Data(int i = 0) : _i(i) {}

std::string operator=(const Data & other) // copy assignment
{
std::cout << "operator= " << _i << std::endl;
_i = other._i;
return "lovite narkomana";
}

int i() const { return _i; }

private:
int _i;
};

int main() {

Data d1(1);
Data d2(2);

std::cout << (d1 = d2) << std::endl;

return 0;
}

// Output:
// operator= 1
// lovite narkomana


Конечно, так делать не рекомендуется.
Кстати, компилятор будет синтезировать оператор присвоения копии с возвратом неконстантной (кто знает почему?) ссылки на левый операнд.
🤔1
Type traits часть 4 (перевод)

std::enable_if и SFINAE

Трейт std::enable_if принимает два параметра - булево значение и тип. Если значение булева параметра является истинным, то результирующая структура (enable_if это параметризированная структура) содержит поле type с типом второго параметра.

Представим такой код:
#include <type_traits>

int main() {
std::enable_if<true, int>::type MyInt{42};
}


Во время компиляции, можно представить что наш enable_if код выльется просто в int, что сделает наш код эквивалентным:
#include <type_traits>

int main() {
int MyInt{42};
}

Обычно мы используем алиас std::enable_if_t, который дает прямой доступ к type:
std::enable_if_t<true, int> MyInt{42};


Давайте посмотрим, что будет если булево значение ложно:
#include <type_traits>

int main() {
std::enable_if<false, int>::type MyInt{42};
}


В этом случае можно представить, что код эквивалентен такому:
#include <type_traits>

int main() {
MyInt{42};
}


Этот код невалиден и мы получим ошибку компиляции. И какая от всего этого польза? Она связана с правилом из мира шаблонов, называемого SFINAE (substitution failure is not an error - «неудавшаяся подстановка — не ошибка»). В общем, правило гласит: Если не получается рассчитать окончательные типы шаблонных параметров функции, компилятор не выбрасывает ошибку. Этот шаблон просто игнорируется и компилятор переходит к другим кандидатам.

Мы можем создать шаблонные функции, которые в зависимости от условий могут привести к ошибке подстановки. Например:
template <typename T,
std::enable_if_t<std::is_class_v<T>, int> = 0>
void Function(T Arg) {}


Если T это класс, то наша подставка будет выглядеть как-то так:
template <typename T, int = 0>
void Function(T Arg) {}

Код выше вполне себе валидный, так что наша функция будет способна обработать наш вызов. Однако, если T это не класс, то шаблон после подстановки будет выглядеть вот так:
template <typename T, = 0>
void Function(T Arg) {}

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

В примере ниже, у нас есть шаблон, который приведет к ошибке подстановки, если тип является классом и другой, если тип не является классом.

#include <type_traits>
#include <iostream>

template <typename T,
std::enable_if_t<std::is_class_v<T>, int> = 0>
void Function(T Arg) {
std::cout << "That was a class\n";
}

template <typename T,
std::enable_if_t<!std::is_class_v<T>, int> = 0>
void Function(T Arg) {
std::cout << "That wasn't\n";
}

class Character {};

int main() {
Function(Character{});
Function(3.14);
}

// Output:
// That was a class
// That wasn't


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

Если код выше выглядит непонятным - то это так. Минимум - это запомнить, что такое правило есть и понять в чем его суть.

Стоит отметить, что в C++20 для упрощения темы шаблонного программирования были добавлены концепты.
👍1
Исходники генератора подземелий для вашей визитки 😁.

https://gist.github.com/munificent/b1bcd969063da3e6c298be070a22b604

#юмор
❤‍🔥1😁1
Type traits часть 5 (перевод)

Условный выбор типа с помощью std::conditional

В заголовке <type_traits> есть шаблон std::conditional, который позволяет нам динамически выбирать тип на этапе компиляции. Он принимает три шаблонных параметра - булево значение, тип для использования, если первый параметр true, и тип, если он false. Результирующий тип доступен в статическом члене type.

Несколько примеров:

using namespace std;

// здесь будет int
conditional<true, int, float>::type

// Можем использовать conditional_t для доступа к ::type напрямую
// здесь будет std::string
conditional_t<true, string, char*>

// Булев параметр может быть любым выражением, известным на момент компиляции
// здесь будет std::string
constexpr bool useStdString{true};
conditional_t<useStdString, string, char*>

// здесь будет std::vector<float>
conditional_t<false, vector<int>, vector<float>>

// здесь также будет std::vector<float>
vector<conditional_t<false, int, float>>


Обычно, std::conditional используется для создания псевдонима/алиаса, который мы затем можем легко использовать в нашем коде:
#include <iostream>
#include <type_traits> // for std::conditional
#include <typeinfo> // for typeid

constexpr bool Use64BitIntegers{true};
using integer =
std::conditional<Use64BitIntegers,
std::int64_t,
std::int32_t>::type;

int main() {
integer MyInt{42};
std::cout << typeid(integer).name() << " ("
<< sizeof(integer) << " bytes)";
}

// Output (варианты, зависит от компилятора):
// __int64 (8 bytes)
// l (8 bytes)
🔥2
software_development_books.png
520.3 KB
Роадмап из книжек про разработку вцелом. Источник.
💘2
Начиная с C++17, в лямбде можно захватить this по значению через *this (просто this захватит объект по ссылке):
class MyClass {
public:
int value = 10;

void printValue() {
auto lambda = [=, *this]() {
std::cout << "Value: " << value << std::endl;
};
lambda();
}
};

Копия объекта может понадобиться например для исполнения лямбды в другом потоке, когда this уже может быть инвалидирован на момент вызова лямбды.
https://habr.com/ru/companies/otus/articles/794821/
👍4
Забавная функция из <numeric> появилась в C++11 - называется std::iota, по всей видимости это греческая буква йота (⍳). Так функция называлась в языке программирования APL (вышел в 1966-м, на лого яблоко, впрочем не надкусанное).
Функция заполняет интервал последовательными значениями (++), начиная с переданного.
#include <iostream>
#include <numeric> // iota
#include <vector>

int main()
{
std::vector<int> v;
v.assign(10, 0);

std::iota(v.begin(), v.end(), 10);

for (auto i : v) {
std::cout << i << " ";
}

return 0;
}
// Output:
// 10 11 12 13 14 15 16 17 18 19
В стандартной библиотеке есть несколько функций для работы с отсортированными контейнерами. Одна их них std::lower_bound:
std::lower_bound(first, last, value) находит первый элемент в интервале [first, last) контейнера, который НЕ расположен до value, то есть не меньше value. Для сравнения используется operator<.
Запомнить результат работы можно так: вернет итератор на позицию, перед которой можно вставить value, не нарушив порядок. Если такой позиции нет, то вернет last (обычно end() контейнера).

#include <iostream>
#include <algorithm>
#include <vector>

int main()
{
std::vector<int> v = {1, 2, 4, 5, 5, 5, 6, 7, 8};

auto print_lower_bound = [&](int x){
auto it = std::lower_bound(v.begin(), v.end(), x);
if (it != v.end()) {
std::cout << "[" << std::distance(v.begin(), it) << "] = "
<< *it << std::endl;
} else {
std::cout << "not found" << std::endl;
}
};

print_lower_bound(5); // 1, 2, 4, 5, 5, 5, 6, 7, 8
// ^

print_lower_bound(3); // 1, 2, 4, 5, 5, 5, 6, 7, 8
// ^

print_lower_bound(0); // 1, 2, 4, 5, 5, 5, 6, 7, 8
// ^

print_lower_bound(9); // 1, 2, 4, 5, 5, 5, 6, 7, 8
// ^

return 0;
}
// Output:
// [3] = 5
// [2] = 4
// [0] = 1
// not found


Так, в примере есть несколько подряд идущих 5, алгоритм вернет итератор на первую (если вставить 5 перед ним, то сортировка не нарушится).
В примере 2, 4 при поиске 3 будет возвращен итератор не на 2, а на 4, тк это первое значение для которого < 3 не сработает (если вставить 3 перед 4, то сортировка не нарушится).
При поиске элемента, большего всех, вернется конец контейнера, тк мы должны вставить его в конец, чтобы сохранить сортировку.
Для того, чтоб найти первый элемент сортированной последовательности, который больше значения, можно применить std::upper_bound. Если сравнить результат работы с lower_bound, то на первый взгляд логика такая же - вернется позиция, перед которой можно вставить элемент, не нарушив сортировки:
#include <iostream>
#include <algorithm>
#include <vector>

int main()
{
std::vector<int> v = {1, 2, 4, 5, 5, 5, 6, 7, 8};

auto print_upper_bound = [&](int x){
auto it = std::upper_bound(v.begin(), v.end(), x);
if (it != v.end()) {
std::cout << "[" << std::distance(v.begin(), it) << "] = "
<< *it << std::endl;
} else {
std::cout << "not found" << std::endl;
}
};

print_upper_bound(5); // 1, 2, 4, 5, 5, 5, 6, 7, 8
// ^

print_upper_bound(3); // 1, 2, 4, 5, 5, 5, 6, 7, 8
// ^

print_upper_bound(0); // 1, 2, 4, 5, 5, 5, 6, 7, 8
// ^

print_upper_bound(9); // 1, 2, 4, 5, 5, 5, 6, 7, 8
// ^

return 0;
}
// Output:
// [6] = 6
// [2] = 4
// [0] = 1
// not found

Однако, как видим, если есть повторяющиеся элементы, то результат разный:
    print_lower_bound(5); // 1, 2, 4, 5, 5, 5, 6, 7, 8
// ^
print_upper_bound(5); // 1, 2, 4, 5, 5, 5, 6, 7, 8
// ^
👍4
std::string::substr возвращает новую строку, созданную как копию подстроки:
basic_string substr( size_type pos = 0, size_type count = npos ) const;

Первый параметр - это позиция (index) начала подстроки, а второй - количество символов (а НЕ позиция конца!). Второй параметр можно не передавать и тогда строка скопируется до конца.

Несколько примеров:
#include <iostream>
#include <string>

int main()
{
std::string str =
"In anger we should refrain both"
" from speech and action."; // Pythagoras

// string substr (size_t pos = 0, size_t len = npos) const;

// С позиции (индекса) == 3 взять 5 символов, получим "anger"
std::string strFrom3Len5 = str.substr(3, 5); // копия подстроки
std::cout << strFrom3Len5 << std::endl;

// Найдем позицию с которой "speech" начинается
std::size_t speechPos = str.find("speech");
if (speechPos != str.npos) { // нашли?
// Если не указать длину, то скопируется до конца
std::cout << str.substr(speechPos) // speech and action.
<< std::endl;
}

size_t strLen = str.size();

// Если pos равен длине строки, то вернется пустая строка
std::cout << str.substr(strLen).size() << std::endl; // 0

// Получить подстроку у пустой строки с 0 тоже можно
std::cout << std::string("").substr(0).size() << std::endl; // 0

try
{
// Если же pos больше длины строки,
// то будет исключение std::out_of_range
std::cout << str.substr(strLen + 1).size() << std::endl;
}
catch (const std::out_of_range& ex)
{

std::cout << ex.what() // invalid string position
<< std::endl;
}

// Если длина len больше, чем число оставшихся символов,
// то подстрока идет до конца, без исключения
std::cout << str.substr(speechPos, 1000) // speech and action.
<< std::endl;
}

// Output:
// anger
// speechand action.
// 0
// 0
// invalid string position
// speech and action.
Type traits часть 6 (перевод) - финал!

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

Представим, что у нас есть шаблонная функция, которая получает объекты, которые либо имеют, либо не имеют функцию render. Напишем трейт is_renderable:
template <typename T>
void Function(T Param) {
if constexpr (is_renderable<T>::value) {
Param.Render();
} else {
std::cout << "\nNot Renderable";
}
}


Трейты из std обычно предоставляют булево значение в виде поля value. Мы можем легко добиться такого же поведения от наших собственных структур, наследуясь от хелперов std::false_type или std::true_type, доступных в заголовке <type_traits>.
Обычно мы хотим value иметь по-умолчанию false, поэтому мы наследуемся от false_type:
template <typename>
struct is_renderable : std::false_type {};


Далее, если для какого-то класса мы хотим значение трейта равное true, мы предоставляем специализацию, которая наследуется от true_type:

template <>
struct is_renderable<MyClass> : std::true_type {};


Теперь с этими четырьмя линиями кода запустим наш трейт:
#include <type_traits>
#include <iostream>

template <typename>
struct is_renderable : std::false_type {};

class Fish {
public:
void Render() {
std::cout << "Fish: ><((((`>";
}
};

template <>
struct is_renderable<Fish> : std::true_type {};

class Sound {};

template <typename T>
void Function(T Param) {
if constexpr (is_renderable<T>::value) {
Param.Render();
} else {
std::cout << "\nNot Renderable";
}
}

int main() {
Fish MyFish;
Function(MyFish);

Sound MySound;
Function(MySound);
}


Вывод:
Fish: ><((((`>
Not Renderable
👍1
std::distance(_InIt first, _InIt last) возвращает "расстояние" - число переходов от первого итератора ко второму.

Для итераторов со случайным доступом (random access), например итератором для vector, эта функция будет работать за O(1) и вернет их разность. Казалось бы, мы можем просто сами найти эту разность итераторов и дело с концом, зачем вообще нужна эта функция?

Однако, для итераторов без случайного доступа (например list), расстояние тоже найдется, но уже путем линейного прохода от одного итератора к другому. Просто разность итераторов тут найти не получится, так как для таких итераторов она просто не определена. Время работы функции в таком случае будет O(n). Таким образом std::distance позволяет найти расстояние по любому типу итераторов, как и многое в std.

Пример:
#include <iostream>
#include <string>
#include <vector>
#include <list>
#include <map>

int main()
{
std::vector<int> v = {10, 20, 30, 40, 50};

std::cout << std::distance(v.begin(), v.end())
<< " " << v.end() - v.begin()
<< std::endl; // 5 5

std::cout << std::distance(
std::next(v.begin()), // ++
std::prev(v.end()) // --
) << std::endl; // 3

auto it = v.begin();
std::advance(it, 3); // продвинемся на 3 вперед
std::cout << std::distance(v.end(), it) << std::endl; // -2

std::list<int> list = { 5, 6, 7, 8 };
std::cout << std::distance(list.cbegin(), list.cend()) << std::endl; // 4

//std::cout << list.end() - list.begin(); // нельзя

std::cout << std::distance(list.cend(), list.cbegin()) << std::endl; // 1 - неверно, тк не random access, UB

std::map<int, int> m = { {1,2}, {3,4}, {5, 6} };
std::cout << std::distance(m.find(3), m.end()) << std::endl; // 2

return 0;
}

// Output:
// 5 5
// 3
// -2
// 4
// 1
// 2


Интересно, что std::distance может вернуть отрицательное значение для random access, если первый итератор дальше второго, что является вполне валидным:
std::distance(v.end(), v.begin()) // -v.size()


Для не random access у меня вернулась ерунда, хотя в таком случае вообще будет UB.
Вспоминаем лямбды)

Неименованный объект-лямбда концептуально (!) трансформируется примерно в код на картинке (имя будет другое и разное для разных лямбд) и так все становится немного понятнее.

Обращаю внимание на const у оператора, это позволит понять, зачем у лямбды может понадобиться mutable.
👍1
Класс, перегружающий оператор вызова (operator() - круглые скобки), позволяет использовать объекты этого типа как функции. Во всем остальном такие классы являются обычными - могут содержать другие методы, хранить состояние и тп.

Напишем в качестве примера класс, который выводит строки в поток через разделитель (пробел по умолчанию). Обращаю внимание, что при перегрузке оператора вызова первые скобки пишутся в названии оператора, а вторые принимают аргументы функции:
#include <iostream>
#include <string>
#include <iomanip>
#include <vector>
#include <algorithm>

class StringPrinter
{
public:
StringPrinter(
const std::string& delim = " ",
std::ostream& stream = std::cout)
: _d(delim), _s(stream)
{

}
~StringPrinter()
{
_s << std::endl;
}
void operator()(const std::string& str)
{
if (_first) {
_first = false;
} else {
_s << _d;
}
_s << std::quoted(str);

}
std::string delim() const
{
return _d;
}
private:
std::string _d;
std::ostream& _s;
bool _first = true;
};

int main()
{
{
StringPrinter print;

print("a");
print("b");
print.operator()("c");
} // "a" "b" "c"

std::vector<std::string> vs = {
"How", "long", "are", "you", "going",
"to", "wait", "before", "you", "demand",
"the", "best", "for", "yourself?"};

std::for_each(vs.cbegin(), vs.cend(), StringPrinter("_"));

return 0;
}

После создания объекта StringPrinter print мы можем "вызвать" его с помощью круглых скобок, а также напрямую print.operator()("c").

Объекты-функции часто используют как аргументы обобщенных алгоритмов, хотя зачастую проще использовать лямбды.
👍1
В заголовке <functional> описаны различные встроенные функциональные объекты.

* Арифметические:
- plus (x + y)
- minus (x - y)
- multiplies (x * y)
- divides (x / y)
- modulus (x % y)
- negate (-x)

* Сравнения (наверное, самые используемые):
- equal_to (x == y)
- not_equal_to (x != y)
- greater (x > y)
- less (x < y)
- greater_equal (x >= y)
- less_equal (x <= y)

* Логические
- logical_and (x && y)
- logical_or (x || y)
- logical_not (!x)

* Битовые и другие

Рассмотрим распространенный пример - сортировку. По-умолчанию, std::sort будет выполнять сортировку по возрастанию, что обеспечит вызов operator<.
Если же мы хотим отсортировать по убыванию (макс. вперед), то можно воспользоваться функциональным объектом std:;greater. На самом деле это шаблонная структура с перегруженным оператором вызова (реализует >), но начиная с C++ 14 тип можно не указывать:
template< class T = void > 
struct greater;


Пример:
#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>

int main()
{
std::vector<int> v1 = { 5, 1, 2, 4, 3 };
auto v2 = v1; // сделаем копию

std::sort(v1.begin(), v1.end()); // 1 2 3 4 5

for (auto i : v1)
std::cout << i << " ";
std::cout << std::endl;

std::sort(
v2.begin(),
v2.end(),
std::greater()); // 5 4 3 2 1

for (auto i : v2)
std::cout << i << " ";
std::cout << std::endl;

return 0;
}
👍4
Интересная особенность библиотечных объектов функций заключается в том, что реализация гарантирует их работоспособность с указателями. Вообще, результат сравнения (<, >) двух несвязанных указателей не определен. Но мы можем захотеть отсортировать н-р контейнер указателей по их адресу. Утверждается, что например сравнивать их на < нельзя, а вот std::less гарантирует верную реализацию (не знаю как):

#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>
#include <string>

int main()
{
std::string s1, s2;
std::vector<std::string*> vp;

vp.push_back(&s1);
vp.push_back(&s2);
vp.push_back(new std::string("memory leak"));

auto vp2 = vp; // copy

std::sort(vp.begin(), vp.end(), [](std::string* l, std::string* r) {
return l < r; // непредсказуемый результат
});

auto print_vector_pointers = [&](std::vector<std::string*> v){
for (auto p : vp) {
std::cout << (size_t)p << " ";
}
std::cout << std::endl;
};

print_vector_pointers(vp);

std::sort(vp2.begin(), vp2.end(),
std::less<std::string*>()); // ok

print_vector_pointers(vp2);

return 0;
}


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

Более того, ассоциативные контейнеры используют для упорядочивания своих элементов std::less, поэтому можно определить set из указателей или map с указателем в качестве ключа. А еще std::sort начиная с C++20 использует std::less, а не operator< для сортировки.

Вывод: иcпользуйте std::less и std::greater - так короче и безопасней.
👍1