Разметка данных для машинного обучения: обзор рынка, методики и компании

Большая доля data science и машинного обучения зависит от чистых и корректных источников данных, поэтому неудивительно, что скорость роста рынка разметки данных продолжает увеличиваться. В этой статье мы расскажем о многих крупных игроках отрасли, а также об используемых ими методиках, чтобы вы могли иметь возможность выбора наилучшего партнёра в соответствии со своими требованиями.

Рынок разметки данных развивается невиданными ранее темпами. В прошлом году его сегмент сторонних решений достиг более 1 миллиарда долларов; ожидается, что он продолжит стабильное расширение в течение следующих пяти лет, и к 2027 году превзойдёт 7 миллиардов долларов. Наблюдая впечатляющий ежегодный рост на 25-30%, некоторые источники, в том числе и Grand View Research, считают, что к 2028 году рынок будет стоить не менее 8,2 миллиарда.

Введение

Разметка данных — это область, возникшая в результате исследований машинного обучения, начавшихся с простых алгоритмов и постепенно превратившихся в современных виртуальных помощников и беспилотные автомобили. Машинное обучение прошло долгий путь — от оригинальной статьи 1950 года Алана Тьюринга об искусственном интеллекте и статьи 1959 года Артура Самуэля про первый алгоритм обучения компьютера до появления Deep Blue компании IBM в 1996 году и Google Brain в 2011 году.

Сегодня разметка данных является неотъемлемой частью разработки машинного обучения (machine learning, ML) и искусственного интеллекта. Без размеченных данных мы бы не могли обучить алгоритмы ML. Именно поэтому многие стремились найти эффективные способы быстрой и экономной разметки данных. Всё просто: чем лучше решение, тем быстрее сможет развиваться ИИ.

Категории программных решений

Для аннотирования данных обучения уровня продакшена существует различное сложное ПО. Однако правильный выбор сделать нелегко. Разнообразные решения проделали долгий путь развития. Их можно разбить на категории по формату, открытости и методикам.

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

Открытость подразумевает доступность и приемлемость цены решения. Она зависит от того, является ли ПО проприетарным (обычно такой вид требует оплаты) или open-source (а следовательно, бесплатным). Существуют проприетарные решения с бесплатным пробным режимом и без него.

Методика разметки — это гораздо более сложная категория, поэтому давайте рассмотрим её подробнее.

Методики разметки данных

С точки зрения методик разметки данных (самой важной для бизнеса категории) сегодня существует широкий выбор решений. Согласно нашей статье, самыми распространёнными являются следующие:

  • Разметка внутри компании

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

  • Аутсорсинг сторонним работникам

Ещё один вариант — это формирование команды внешних разметчиков, большинство из которых является фрилансерами. Их можно найти на специализированных сайтах наподобие UpWork, а также в соцсетях, например, в LinkedIn, Facebook и Twitter. Для начала компании необходимо выстроить целостный рабочий процесс с участием этих специалистов. Затем ей нужно скоординировать их обязанности, согласовать подходящее ПО и написать чёткие инструкции.

  • Аутсорсинг специализированным компаниям

Эта методика похожа на предыдущую, однако в данном случае вместо множества разметчиков компании нанимают только одну организацию. Поэтому эта методика более дорога, зато теоретически менее времязатратна и более надёжна с точки зрения точности. Каждая из таких специализированных компаний сама выбирает программные инструменты и методики, при этом не всегда раскрывая их заказчикам.

  • Синтетическая разметка

При такой методике генерируются имитируемые данные обучения, напоминающие по своим базовым параметрам реальные данные. Такие данные можно относительно быстро сгенерировать и использовать для разметки. В рамках синтетической разметки существует три подмодели: Generative Adversarial Networks (GAN), AutoRegressive models (AR) и Variational Autoencoders (VAE). Все три имеют применение на практике, от распознавания мошенничества в финансовой сфере до создания медицинских массивов данных. Однако для выполнения таких моделей требуются серьёзные вычислительные ресурсы, доступные не каждому отделу ИТ.

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

  • Краудсорсинг

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

7 вопросов, которые нужно задать себе

При выборе подходящего ПО нужно быть хорошо осведомлённым о плюсах и минусах каждого пакета, чтобы минимизировать вероятность возникновения мешающих бизнесу проблем. Можно начать с ответа на следующие простые вопросы:

Дорога ли эта платформа? Каково её соотношение цены и качества?

Будут ли затраты времени минимальными?

Есть ли в ней встроенные механизмы контроля качества?

Не слишком ли сложно обучать пользователей?

Предоставляет ли она исчерпывающие инструкции для новичков?

Поддерживаются ли все типы данных?

Требует ли она большой вычислительной мощности?

Обзор инструментов разметки данных

На сегодняшний момент существует более десятка различных инструментов и платформ разметки данных. Давайте их рассмотрим.

Компания: Appen

Веб-сайт: https://appen.com

Краткое описание:

Appen предлагает решения для бизнесов по управлению данными продуктов. Компания собирает и размечает данные для создания и совершенствования собственных систем искусственного интеллекта. Appen работает в таких областях, как финансовые услуги, розничная торговля, здравоохранение и госсектор. Заявляется, что на Appen работает 1 миллион разметчиков данных из 170 стран.

Тип данных: Большинство, включая текст, изображения, звук и видео
Открытость: Проприетарная (есть пробная версия)
Методика: Краудсорсинг (подкатегории: по запросу, удалённый, защищённый и выезд сотрудников в компанию) на платформе ADAP + платформа для услуг найма (собственные специалисты клиента)
Отрасли: Технологическая, автомобильная, финансовые услуги, здравоохранение, госсектор и розничная торговля
Соотношение цены и качества: Выше среднего
Оптимальность времязатрат: Высокая
Инструкции для новичков: Да (набор учебных материалов)
Скорость обучения пользователей: Средняя или высокая (в зависимости от подкатегории)
Механизмы контроля качества: Параметры контроля качества: минимальное время на страницу, максимальное количество решений на одного участника, отключение Google Translate для участников и правила распределения ответов.

Источник: веб-сайт Appen

Вычислительные мощности: От клиента не требуются, за исключением случаев работы с выездом к клиенту

Компания: Lionbridge AI

Веб-сайт: https://lionbridge.ai/

Краткое описание:

Находящаяся в штате Массачусетс компания Lionbridge предлагает услуги в сфере сбора, аннотирования и валидации данных, в том числе текста, звука, видео, изображений и географической информации. Компания гордится тем, что поставляет качественные данные, размеченные людьми. Она задействована во множестве отраслей, включая автомобильную, медицинскую и сферу электронной коммерции. (Приобретена канадской TELUS Corporation)

Тип данных: Большинство, включая текст, изображения, звук, видео и данные географической локализации
Открытость: Проприетарная
Методика: Краудсорсинг и аутсорсинг (для задач с узкой специализацией) / собственная платформа с обширными резервами собственных сотрудников и работников на аутсорсе/краудсорсе
Отрасли: Технологическая, коммуникационная и медийная, финтех и финансовые услуги, путешествия и гостиничный бизнес, игры, электронная коммерция и здравоохранение
Соотношение цены и качества: От среднего до хорошего (зависит от задачи и методики)
Оптимальность времязатрат: От средней до высокой (зависит от задачи и методики)
Инструкции для новичков: Да (публикации и пошаговый разбор)
Скорость обучения пользователей: Средняя или высокая (зависит от методики)
Механизмы контроля качества: Встроенная валидация, выборочная проверка и система старшинства работников для обеспечения максимального качества данных.
Вычислительные мощности: От клиента в большинстве случаев не требуются

Компания: Scale

Веб-сайт: https://scale.com/

Краткое описание:

Находящаяся в Кремниевой долине Scale — одна из лучших компаний, гарантирующая предоставление услуг высококачественной разметки данных, начиная от аннотирования и проверки (в том числе при помощи масштабируемых, всеобъемлющих и современных решений) до генерации синтетических наборов данных. Scale сотрудничает с компаниями в сферах робототехники, распознавания речи и языков, беспилотных автомобилей, электронной коммерции и обработки документов, а также многих других.

Тип данных: Массив данных 3D-датчиков, текст, изображения, видео, картография
Открытость: Проприетарная (доступна бесплатная пробная версия)
Методика: «Платформа данных для ИИ». Сочетание инструментов и контроля человеком.
Отрасли: Технологическая, розничная, госсектор, робототехника, электронная коммерция
Соотношение цены и качества: Среднее
Оптимальность времязатрат: От средней до высокой
Инструкции для новичков: Да (руководства и краткие введения)
Скорость обучения пользователей: Средняя
Механизмы контроля качества: Всем задачам назначаются дополнительные слои при проверке и людьми, и моделями машинного обучения. Системы обеспечения качества отслеживают и предотвращают ошибки. В соответствии с требованиями клиента предоставляются различные уровни контроля людьми и обеспечения консенсуса.

Источник: веб-сайт Scale

Вычислительные мощности: От клиента не требуются, если не заказаны конкретные специализированные решения

Компания: «Толока» (Toloka)

Веб-сайт: https://toloka.ai/

Краткое описание:

Компания была основана в 2014 году и с тех пор разрослась до работы с двумя тысячами клиентов. «Толока» имеет несколько миллионов пользователей (называемых «толокерами») более чем в ста странах. Двести тысяч из них активны ежемесячно и готовы к выполнению задач разметки данных по заказу. Впоследствии для разработки беспилотного транспорта, NLP, чат-ботов и голосовых помощников, а также систем электронной коммерции применяются обученные алгоритмы машинного обучения. Кроме того, «Толока» активно участвует в исследованиях ИИ. Команда проекта регулярно делится своими результатами на лучших конференциях по ML/AI/DS и мастер-классах по всему миру.

Тип данных: Большинство, в том числе текст, изображения, звук и видео
Открытость: Проприетарная
Методика: Краудсорсинг
Отрасли: Электронная коммерция, розничная торговля, автомобильная, кибербезопасность, банкинг, спорт, юридические информационные технологии, исследования, производство, здравоохранение.
Соотношение цены и качества: Выше среднего
Оптимальность времязатрат: Высокая
Инструкции для новичков: Да (база знаний)
Скорость обучения пользователей: От средней до высокой
Механизмы контроля качества: Выбор исполнителей: предварительная фильтрация, обучение, входное тестирование. Синхронный контроль качества: проверки поведения (CAPTCHA, мониторинг скорости, проверка определённых действий), проверка качества распределения работ (мажоритарное голосование, контрольные задания). Асинхронный контроль качества: контроль распределения работ, методики интеллектуальной агрегации.

Источник: веб-сайт «Толоки»

Вычислительные мощности: От клиента не требуются

Компания: MTurk (Amazon Mechanical Turk)

Веб-сайт: https://www.mturk.com/

Краткое описание:

MTurk — одна из крупнейших и известных современных платформ краудсорсинга. После своего основания в 2005 году компания быстро расширялась, в том числе и благодаря популярности бренда Amazon. Имея большой резерв участников (называемых «Turkers»), сто тысяч из которых доступны в любой момент времени, компания обслуживает клиентов по всему миру и делает упор на HIT (Human Intelligence Tasks, задачи для человеческого разума).

Тип данных: Большинство
Открытость: Проприетарная
Методика: Краудсорсинг
Отрасли: Большинство отраслей
Соотношение цены и качества: Выше среднего
Оптимальность времязатрат: Высокая
Инструкции для новичков: Да (учебные материалы и служба поддержки)
Скорость обучения пользователей: От средней до высокой
Механизмы контроля качества: Система квалификаций позволяет выбирать и создавать конкретные критерии требований к работникам в вашем проекте. В MTurk есть три типа заранее заданных квалификаций — Masters, System и Premium Qualifications.
Вычислительные мощности: Для клиента не требуются

Компания: Hive

Веб-сайт: https://thehive.ai/

Краткое описание:

Ещё одна базирующаяся в Кремниевой долине компания Hive была основана в 2013 году. Она имеет две основных бизнес-парадигмы: Hive Models (ИИ-решения «под ключ» для предварительно обученных моделей глубокого обучения) и Hive Data (внутренние решения аннотирования данных для снабжения и разметки моделей ML). Утверждается, что для последней у компании есть 2 миллиона участников, работающих на её платформе. В 2021 году компания собрала 85 миллионов долларов дополнительного финансирования.

Тип данных: Множество, в том числе текст, изображения, звук, видео, облако 3D-точек
Открытость: Проприетарная
Методика: Краудсорсинг и аутсорсинг + генерация данных на заказ
Отрасли: Технологическая, автомобильная, производственная, гостиничная, розничная торговля, электронная коммерция
Соотношение цены и качества: От среднего до хорошего
Оптимальность времязатрат: От средней до высокой (зависит от задачи)
Инструкции для новичков: Да (+ поддержка от назначаемых менеджеров проектов)
Скорость обучения пользователей: Высокая
Механизмы контроля качества: Интегрированная разметка вручную. Валидация пограничных случаев в реальном времени, дополнительное обогащение данных при необходимости. Тестирование проектов по небольшим выборкам данных.
Вычислительные мощности: В большинстве случаев от клиента не требуются

Компания: Webtunix AI

Веб-сайт: https://www.webtunix.com/

Краткое описание:

Расположенная в Нью-Йорке Webtunix, основанная индийскими предпринимателями — это консалтинговая компания в сфере Big Data, помогающая бизнесам достигать своих целей в области ИИ. В их число входят различные услуги разметки данных в компьютерном зрении, звуке, NLP и семантической сегментации, а также аннотирования видео и разметки линиями. Компания была основана в 2015 году.

Тип данных: Большинство, в том числе текст, изображения и видео
Открытость: Проприетарная
Методика: Специализированные программные инструменты под конкретные задачи с методологией human-in-the-loop (индивидуальный аутсорсинг и программирование данных в зависимости от технологий)
Отрасли: Сельскохозяйственная, автомобильная, электронная коммерция, кибербезопасность, здравоохранение, банкинг, спорт
Соотношение цены и качества: Среднее
Оптимальность времязатрат: От средней до высокой
Инструкции для новичков: Да (поддержка 24/7)
Скорость обучения пользователей: Высокая (подробные руководства)
Механизмы контроля качества: Квалифицированные живые асессоры и кураторы, функции автоматизации.
Вычислительные мощности: В большинстве случаев от клиента не требуются

Компания: iMerit

Веб-сайт: https://imerit.net/

Краткое описание:

iMerit была основана в 2012 году. Компания сочетает MAA и ручное аннотирование для упрощения бизнес-процессов и предоставления размеченных данных корпоративным клиентам. В основном компания работает в сферах компьютерного зрения и NLP и использует собственную команду опытных специалистов. iMerit, имеющая более четырёх тысяч сотрудников, сертифицирована по SOC 2 и обладает ещё одним сертификатом о защите данных — ISO 27001:2013. При разметке данных компания может использовать свои собственные инструменты аннотирования, инструменты клиента или сторонние инструменты.

Тип данных: Текст, изображения, звук, видео, геопространственные данные
Открытость: Проприетарная (бесплатная проверка на выборке набора данных)
Методика: Многоуровневый подход: за оценкой проекта следует аутсорсинг, краудсорсинг или программирование данных
Отрасли: Транспортная, геопространственная, медицинская, электронная коммерция, сельское хозяйство, госсектор
Соотношение цены и качества: От среднего до хорошего (зависит от задачи и методики)
Оптимальность времязатрат: От средней до высокой
Инструкции для новичков: Да (доступна всеобъемлющая поддержка клиентов)
Скорость обучения пользователей: От средней до высокой (зависит от задачи и методики)
Механизмы контроля качества:

Источник: веб-сайт iMerit

Вычислительные мощности: В большинстве случаев от клиента не требуются

Компания: CloudFactory

Веб-сайт: https://www.cloudfactory.com/

Краткое описание:

Базирующаяся в Рединге (Англия) CloudFactory заявила о своём присутствии на рынке в 2010 году и с тех пор предлагает аутсорсинговые и краудсорсинговые услуги разметки данных клиентам по всему миру. Компания специализируется на компьютерном зрении, NLP, наполнении данными и обогащении данных. CloudFactory называет свои конвейеры поставок «виртуальными сборочными линиями», на которых трудится примерно 1 миллион человек из развивающихся стран. Компания «выращивает их как лидеров, чтобы они способствовали решению проблемы бедности в своём обществе».

Тип данных: В основном текст, изображения, видео и звук
Открытость: Проприетарная (бесплатное демо)
Методика: Краудсорсинг и аутсорсинг (платформа управления рабочей силой), решения «под ключ» или платформа для найма/расширения рабочей силы
Отрасли: Розничная торговля, транспортная, индустрия развлечений, образовательная, фитнес, сельскохозяйственная, производственная
Соотношение цены и качества: От среднего до хорошего (зависит от методики)
Оптимальность времязатрат: От средней до высокой (зависит от методики)
Инструкции для новичков: Да (блог, вебинары, библиотека ресурсов)
Скорость обучения пользователей: Высокая
Механизмы контроля качества: Описаны в The Outsourcers’ Guide to Quality на веб-сайте компании.

Источник: веб-сайт CloudFactory

Вычислительные мощности: В большинстве случаев от клиента не требуются

Компания: Clickworker

Веб-сайт: https://www.clickworker.com/

Краткое описание:

Находящаяся в Германии Clickworker, основанная в 2008 году — это платформа краудсорсинга, которая специализируется на компьютерном зрении и NLP. Имея почти 3 миллиона участников (называемых «Clickworkers») из 136 стран, компания заявляет, что уже завершила более 1 миллиона проектов по разметке данных. Также у компании есть специализированное приложение Clickworker для смартфонов и планшетов.

Тип данных: Большинство, в том числе текст, изображения, звук и видео
Открытость: Проприетарная (доступна бесплатная пробная версия)
Методика: Краудсорсинг (удалённое управление, самообслуживание, API)
Отрасли: Электронная коммерция, розничная торговля, исследования и Big Data
Соотношение цены и качества: Выше среднего
Оптимальность времязатрат: Высокая
Инструкции для новичков: Да (набор учебных материалов)
Скорость обучения пользователей: Высокая
Механизмы контроля качества: Специальные процедуры обеспечения качества, например, статистическое тестирование процессов, аудиты и взаимное рецензирование. Clickworkers выбираются согласно уровню их обучения и результатов тестов, а также результатов их работы в прошлом.

Источник: веб-сайт Clickworker

Вычислительные мощности: В большинстве случаев от клиента не требуются

Подведём итог

Основной сложностью в разметке данных является их качество. Эта сложность становится особенно актуальной при создании сложных моделей машинного обучения, в частности, при параллельном выполнении нескольких моделей. Сегодня уже существует множество готовых инструментов разметки, поэтому главной сложностью для дата-саентистов является подбор ПО для решения задачи — определение того, в каких ситуациях одни инструменты имеют преимущество перед другими. Это приводит к одному простому вопросу: как выполнить разметку данных с наименьшим объёмом усилий, времени и денег, в то же время обеспечив максимально возможные результаты?

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


ссылка на оригинал статьи https://habr.com/ru/post/599323/

gRPC в .NET — рецепты счастья

Массовый переход от монолитов к микросервисам решает ряд проблем:

  • раздельный деплой и рефакторинг;

  • удобное масштабирование частей системы;

  • прозрачное разграничение ответственности команд;

  • снижение бласт-радиуса;

  • снижение когнитивной нагрузки на разработчика.

При этом создает другие проблемы: взаимодействие сервисов существенно сложнее и дороже, чем взаимодействие объектов в памяти. Частично упростить его можно с помощью протокола gRPC.

gRPC дает возможность зафиксировать в репозитории контракты межсервисных вызовов, строгую типизацию, стриминг, кросс-платформенную кодогенерацию и множество других полезных для межсервисного общения вещей.

Из этой статьи вы узнаете, когда стоит применять gRPC, а когда лучше воздержаться, как решаются типичные задачи, включая конфигурирование, отладку, healthcheck, а также то, о чем умалчивает документация. 

По материалам выступления на конференции DotNext:

Меня зовут Михаил Кузнецов, сейчас я senior engineering manager платформы автоматизации маркетинга Mindbox.

Основы gRPC: что это такое и какие проблемы решает

Что такое gRPC. gRPC — кросс-платформенный протокол удаленного вызова процедур от Google. Транспорт осуществляется по HTTP/2. Неплохо поддерживается в .NET Core, начиная с версии 3.1. В версии .NET 5.0 появилось много расширений и дополнительных настроек — всё стало еще лучше.

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

  • Сложнее с точки зрения кода. Если экземпляр класса А вызывает экземпляр класса В, то это достаточно тривиально. Если это сетевой вызов, надо сериализовать пейлоад и получить ответ, в котором возможны сетевые ошибки. Этот пейлоад также нужно отправить в канал и получить из канала. В общем, требуется достаточно много действий.

  • Хуже перформанс. Вызвать function call намного дешевле, чем гонять параметры и результаты работы по сети. 

  • Усложняется обработка ошибок. Появляется сразу несколько дополнительных слоев, где что-то может пойти не так. Например, сериализатор сериализовал не так (формат даты / числа с точкой вместо запятой / snake_Case вместо PascalCase), или «икнула» сеть, или сервис не отвечает, или сервис отвечает, но слишком долго.

В качестве альтернативы синхронного взаимодействия иногда возникает желание использовать шины данных, брокеры (Kafka, RabbitMQ и аналоги), потому что зачастую они уже есть в инфраструктуре и проблему кое-как решают. Этот подход не очень удобный: асинхронный eventual consistency-вариант вместо синхронного вызова — это костыль. 

Чем gRPC лучше REST. У REST сразу несколько недостатков:

  • REST — не Contract First: сложно управлять контрактами и целостностью API. По сути целостность контрактов нельзя контролировать. Можно прикрутить Swagger, инструмент для генерации документации по коду. То есть у вас уже есть код и генерируется документация. А хотелось бы по понятным причинам наоборот: чтобы код генерировался по документации.

  • Много бойлерплейт-кода: создать HTTP-клиент, HTTP client factory, договориться о сериализации (JSON, XML или другие варианты), учесть различные форматы дат, чисел и так далее, кейсинг. Соответственно, много возможностей для возникновения ошибок — нужно писать DTO-классы специально для транспорта.

  • У REST не очень хороший перформанс в сравнении с gRPC.

  • Использовать REST не очень оптимально с точки зрения сети, потому что будет летать JSON или XML. Правда, этот момент можно улучшить: использовать msgPack или похожие средства оптимизации транспорта.

Что предлагает gRPC

Contract First. Сначала договариваемся о том, что и куда пересылаем, описываем это строго в protobuf-файле, кладем в репозиторий, и, по сути, с этого момента он зафиксирован. При желании от этого решения можно отказаться и пользоваться Code First. Но это применимо, только если у вас весь бэкенд на .NET: можно создать те самые DTO, повесить на них атрибуты привычным вам способом, как с тем же JSON или XML. 

Строгая типизация. Если вы пишете на .NET, скорее всего, любите строгую типизацию —  здесь она есть 🙂 

Автогенерация клиентского и серверного кода. Описываете контракт, используя штатный инструмент во время билда, и у вас создается либо клиент, либо сервер, либо и то и другое — в зависимости от настройки. 

Это дает сразу несколько преимуществ:

  • При кодогенерации не нужно писать код самому и поддерживать его. 

  • Нельзя сбилдить проект, не поменяв клиент и сервер, если поменялся контракт. 

Контракт может развалиться только в одном сценарии: у вас два сервиса (клиент и сервер), и у них разъехались версии, то есть один перебилдился, обновился и задеплоился, а второй — нет. Понятное дело, с этим сложно что-то сделать — только тестировать. Во всех остальных сценариях на билде перегенерируется весь соответствующий код — контракт не развалится.

  • Встроена сериализация из коробки.

Она достаточно эффективна и с точки зрения перформанса, и с точки зрения транспорта: пейлоад, который летает по сети, существенно компактней обычного JSON. 

  • У gRPC достаточно лаконично сделана обработка ошибок. Какие-то вещи она маскирует и упрощает, но для практического применения ее вполне достаточно. 

  • gRPC предлагает удобный стриминг, то есть возможность создать стрим в любую сторону: с сервера на клиент, с клиента на сервер и даже двунаправленный стрим — дуплекс. 

Поддержка в .NET

Библиотека gRPC C# 

Это достаточно тонкая обертка вокруг unmanaged-библиотеки. Есть основная библиотека на С++ и оболочка для вызовов на .NET. Она обладает максимальной функциональностью и минимальными удобствами. 

Как в любой обертке, из нее торчат уши. Например, все настройки сделаны не в виде строго типизированных свойств с соответствующими ограничениями, а в виде строковых констант. То есть вы передаете строку с названием настройки и строку с ее значением. У вас также будет unmanaged-код, которого обычно хочется избежать: ошибки в unmanaged-коде вызваны как возможными багами самой библиотеки, так и неправильным использованием. Эти ошибки хуже отлавливаются, хуже дебажатся и так далее.

Использовать библиотеку я советую местами, не в продакшене, например писать эмуляторы того, что потом будет в продакшене, подключать для мокирования, для тестов. Стартануть на ней максимально просто, это делается буквально в одну строку.

Альтернативная библиотека gRPC for .NET

Это мейнстримное решение, поддерживаемое Microsoft, — оно подробно описано в документации. Тут быстрый старт немного сложнее, но есть и плюсы: она полностью управляемая. Всё, что вы настраиваете и конфигурируете, строго типизовано. Эта библиотека очень хорошо интегрируется в пайплайны ASP .NET Core, то есть в DI многие вещи получится сделать привычным образом. Дальше рассказывать я буду в основном о ней, потому что это мейнстримное решение.

Транспорт происходит по HTTP2 — не будет сюрпризом, что внутри gRPC-клиента сидит знакомый всем System.Net.Http.HttpClien. Многие настройки транспортного уровня находятся в привычных для всех полях и классах, которые составляют HTTP-клиент. Если эта настройка не выглядит gRPC-специфичной, то, скорее всего, вы найдете ее там же, где и раньше. Это различные таймауты, Keep Alive, авторизация и тому подобные вещи.

Многие серверные gRPC-настройки также настраиваются стандартным способом, как и настройки Kestrel. Если вы делаете gRPC-сервер с помощью gRPC for .NET в .NET-приложении, то конфигурацией Kestrel вы также управляете поведением gRPC-сервера.

Protobuf-контракты

Выглядит это примерно так:

message Outcome{   string id = 1;   int32 selection_kind = 2;   double price = 3;   bool disabled = 4;   map<string, string> properties = 5;   string raw_id = 6; }

Контракт protobuf строго типизованный и строго определяет последовательность полей. Это нужно, например, по причинам совместимости старых и новых версий API. Он поддерживает nullable, коллекции, enum, дефолтные значения, вложенные типы. 

Немного чисел: на официальном сайте Google заявляет, что контракт protobuf до 10 раз компактнее, до 100 раз быстрее JSON в сыром виде. Могу сказать, что это похоже на правду: по замерам он кратно компактнее и в 20–30 раз быстрее.

Информация для любителей MessagePack: по бенчмаркам скорость и компактность — на неразличимо близком уровне.

Кросс-платформенность и кодогенерация. Генерируются и клиент, и сервер, и сериализация, и десериализация, и DTO, то есть весь этот слой будет у вас готов — его не нужно писать.

Поддерживается несколько популярных платформ: C#, Go, Java, Python, Kotlin, PHP, Ruby. Поддерживается не только бэкенд — стандартные стеки, но и клиентские мобильные платформы.

Отдельно стоит отметить, что, по опыту, gRPC снимает проблемы с кейсингом, форматированием, опечатками в именах полей. Снимает также проблемы с форматами примитивных типов: дат, чисел и так далее. Это особенно актуально, если на бэкенде существуют разные технологические стеки. Естественно, все ошибки можно отловить на тестировании, но проще вообще не бороться с ними — кодогенерация одним инструментом решает эту проблему.

Код обоих сервисов: и клиента, и сервера будет синхронизирован с их контрактом. Это гарантируется компилятором. С вашей стороны нужно только гарантировать, что если вы поменяли контракт, на стенд выкатываются синхронизированные версии. Это можно обеспечить, например, интеграционными тестами. Все, что уровнем ниже, может быть проверено на compile time.

Альтернативы gRPC

Kafka. Основной минус — если есть topic, на который вы подписываетесь, то вы не можете фильтровать данные и сообщения, которые туда падают. Приходится читать все и разбирать на клиенте.

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

Как это можно было бы сделать с помощью топиков?  Сервер отслеживает, кто сейчас онлайн, и скидывает информацию о них в топик, отфильтровывая обновления данных по тем пользователям, которые офлайн.

При этом, если разные клиенты хотят читать про конкретных пользователей, то им всё равно нужно будет выбирать, либо дискардить кучу сообщений, либо заводить по топику на пользователя или по топику на хэш от ID пользователя. А это уже динамические топики, что достаточно сложно и дорого. Если вы пробовали делать тысячи партиций, а топик — это минимум одна партиция в Kafka, то, возможно, уже сталкивались с этой проблемой. 

Кроме того, создать и удалить топик — достаточно ресурсоемкая операция, а подписаться на новый топик занимает несколько секунд. В итоге получается очень шаткая и непонятно зачем нужная конструкция. Как это выглядит со стримингом gRPC? Мы создаем стрим типа GetUserInfoById, передаем ID, в ответ получаем стрим по конкретному пользователю.

Если тот, кто кормит нас данными (продюсер), сломался и перестал обновляться, то в случае с Kafka нужны какие-то внешние инструменты, возможно, healthchecks продюсера. Их нужно отслеживать — получается достаточно сложная и хрупкая конструкция. В случае с gRPC все проще: разрывается соединение, клиент узнает об этом очевидным образом и может среагировать на проблему. 

OpenAPI. Предлагает контракты: они не обязательны, но их можно использовать. Позволяет осуществлять кодогенерацию клиента, но так как это не gRPC, а сырой HTTP, у него несколько хуже перформанс, нагрузка на сеть несколько выше, а со стримингом все несколько сложнее. Например, там вообще нет дуплекса. В остальном OpenAPI во многом схож с gRPC.

HttpClient.GetStreamAsync. У него тоже есть стриминг, но единственное, что про него можно сказать, — им можно что-то стримить. Остальные потребности он не закрывает: остаются все те же проблемы с типизацией, с контрактами, сериализацией. Все нужно делать вручную, да и сам стрим тоже нужно обслуживать вручную. В общем, вариант достаточно спорный.

Подводные камни gRPC

Издалека все выглядит хорошо, а на деле возникают сложности. Ниже минусы в первом приближении:

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

Не интегрируется с браузерами. Эту проблему можно решить сторонними проектами, их уже два. Первый называется gRPC HTTP API — это, по сути, генерация HTTP end point и контроллера поверх gRPC endpointer. Второй проект называется HTTP Gateway — это отдельно стоящий сервис, который работает как прокси для gRPC-сервиса.

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

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

HealthCheck gRPC-сервиса

Если вы пользуетесь Kubernetes, то знаете, что такое HealthChecks. Если коротко, это endpoints, через которые можно опросить сервис и приблизительно понять, что с ним происходит.

Есть так называемые Liveness/Readiness probes. Первая отвечает на вопрос, жив ли сервис, а вторая — готов ли он обрабатывать наши запросы. Даже если у вас нет Kubernetes, такие эндпойнты могут быть полезны для внешнего мониторинга и логирования состояния сервиса. 

Для gRPC-сервисов есть стандарт. Он описан прямо в основном репозитории gRPC. Пакет, реализующий этот стандарт по .NET, называется gRPC HealthCheck. Он несложно устроен: чтобы иметь у себя хелсчеки, надо подключить пакет и реализовать интерфейс. Все, что нужно сделать, когда хотим поменять статус сервиса, — выставить статус специальному объекту healthGrpcService. Статусов всего четыре вида: serving, not_serving, unknown и service_unknown.

Первый параметр string.Empty — название сервиса для ситуации, когда в одном процессе находится несколько независимых gRPC-сервисов. Управлять их статусами можно независимо. Насколько это бьется с микросервисной архитектурой — вопрос. Но инструмент для такой ситуации есть. Вместо string.Empty можно передавать имя сервиса — в пробах будут пропагироваться сервисы, соответствующие своим статусам. 

gRPC и KeepAlive 

C KeepAlive все оказалось не так просто. Здесь тоже есть стандарт в виде таблицы с описанием: 

gRPC_ARG_KEEPALIVE_TIME_MS — время, которое проходит между KeepAlive-пингами. По дефолту клиент ждет бесконечно долго: он вообще ничего не шлет, а сервер шлет пинги раз в два часа. Очевидно, это сделано для того, чтобы убивать уснувших или отвалившихся клиентов.

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

gRPC_ARG_KEEPALIVE_PERMIT_WITHOUT_CALLS — по умолчанию 0. Это означает, что, если у вас не происходит отправок бизнес-сообщений, то и KeepAlive слаться не будут. Если указан 1, то KeepAlive слаться будут. 

gRPC_ARG_HTTP2_MAX_PINGS_WITHOUT_DATA. Регламентирует максимальное количество KeepAlive-пингов, не перемежающихся бизнес-сообщениями. 

Резюмируем: по дефолту ничего не шлется. Если сделаем time не бесконечным, а сколько-то секунд, например одну минуту, то раз в минуту у нас будут слаться пинги, но только если происходят постоянные бизнес-вызовы. Если стрим висит достаточно долго, то все равно ничего не будет посылаться. Если мы разрешим отправку, выставив третий параметр в единицу, то пошлется два пинга и после этого опять KeepAlive остановится. 

Здесь достаточно неприятный момент: если мы хотим это изменить, то придется передеплоить все серверные gRPC-части. Если мы увеличим gRPC_ARG_HTTP2_MAX_PINGS_WITHOUT_DATA только у клиентов с 2 на 10 или на бесконечность, то после второго сообщения непереконфигурированный сервер будет прекращать обрабатывать стримы, посылать сообщение GOAWAY, и на этом все будет заканчиваться. С точки зрения менеджмента и деплоя, достаточно сложное изменение. Нужно задеплоить все сервера, потом всех клиентов. А если вы хотите что-то поменять или откатить, то придется делать все то же самое, только в обратном порядке.

Инструментарий

При разработке REST API многие пользуются Postman или аналогичными инструментами. Естественно, для gRPC хотелось бы иметь что-то похожее.  

Есть BloomRPC. Мы пробовали искать что-то еще, потому что в сравнении с Postman он выглядит слишком примитивно для главного инструмента, решающего задачи отладки сервисов. Но другого инструмента не нашлось. Базовые потребности он закрывает. Так как gRPC работает с протоконтрактами, работа с BloomRPC достаточно простая и удобная. Загружаем протофайл — это сразу дает возможность вызвать конкретный сервер, выбрать endpoint. У нас все готово для того, чтобы вбить значение в пейлоад. Нужно только указать URL, куда мы будем коннектиться. 

Немного о производительности

Я говорил о Protobuf, но это не все, что определяет протокол. Есть еще затраты в .NET-коде: распарсить, сериализовать, послать, получить и так далее. Я намеренно не хотел синтетики, числа будут из реального приложения, но достаточно легковесно обрабатывающего каждое конкретное сообщение. 

Накладные расходы на gRPC даже на фоне легковесной обработки практически не измеримы. Это проценты от миллисекунд. С другой стороны, минимум сотни сообщений, обработанные на одном ядре в секунду. Помимо самого gRPC, происходит бизнес-обработка, мы что-то складываем в Mongo, продьюсим в Kafka, что-то считаем. С учетом накладных расходов на gRPC мы наблюдали сотни сообщений в секунду, что нас вполне устраивало. 

Мы столкнулись с проблемами не самого gRPC, а LargeObjectsHeap: если посылать действительно большие сообщения, большие пейлоады, у вас будет фрагментироваться LOH и будут происходить частые сборки мусора во втором поколении. С этим ничего не сделать, только уменьшать размер сообщения до вменяемого, бить на чанки, делать рефакторинг, иногда что-то менять по бизнесу. 

Еще в обновлении библиотеки появился extension class UnsafeByteOperations. Он позволяет хотя бы на клиенте не копировать большой пейлоад в большой пейлоад формата gRPC, который потом полетит по сети, а взять готовый массив с большим пейлоадом, легковесно обернуть его и передать. На мой взгляд, это все равно достаточно слабое решение, потому что для серверной части UnsafeByteOperationsвообще нет. Скорее всего, проблемы останутся. 

Мы поднимали две ноды, клиентскую и серверную, большое количество стримов, эмулировали некоторую нагрузку. Удавалось параллельно выдержать несколько десятков тысяч gRPC-стримов, которые еще и посылают сообщения, а не просто висят мертвым грузом, но посылают их не слишком часто, раз в несколько секунд. Тем не менее десятки тысяч стримов между нодами работали хорошо. Ни по памяти, ни по CPU безумных чисел не было, речь шла о нескольких ядра и паре Гб в режиме работы, то есть когда нагрузка спадает, то и расход памяти существенно сокращается. 

Подведем итоги

Если у вас .NET Core 3.1 и выше, микросервисы на бэкенде, они написаны на разных стеках и всё это причиняет боль, стоит рассмотреть gRPC, оценить его применимость для вашего проекта. Возможно, он будет вам полезен. 

Следующая .NET-конференция DotNext пройдет онлайн 7 и 8 апреля.


ссылка на оригинал статьи https://habr.com/ru/company/mindbox/blog/597183/

Ссылки и ссылочные типы в C++


Продолжаем серию «C++, копаем вглубь». Цель этой серии — рассказать максимально подробно о разных особенностях языка, возможно довольно специальных. Это пятая статья из серии, список предыдущих статей приведен в конце в разделе 6. Серия ориентирована на программистов, имеющих определенный опыт работы на C++. Эта статья посвящена ссылкам и ссылочным типам в C++.

Термин «ссылка» широко используется и в обыденной жизни, в компьютерных и других науках и поэтому его смысл сильно зависит от контекста использования. В языках программирования под ссылкой понимают небольшой объект, главная задача которого обеспечить доступ к другому объекту, расположенному в другом месте, имеющему другой размер и т.д. Объекты ссылки удобно использовать на стеке, они легко копируются, что позволяет получить доступ к объекту, на который эта ссылка ссылается, из разных точек кода. В той или иной форме ссылки поддерживаются во всех языках программирования. В ряде языков программирования, таких как C#, Java, Pyton и многих других, ссылки, по существу, являются концептуальным ядром.

В C роль ссылок играют указатели, но работать с ними не очень удобно и в C++ появилась отдельная сущность — ссылка (reference). В C++11 ссылки получили дальнейшее развитие, появились rvalue-ссылки, универсальные (передаваемые) ссылки, которые играют ключевую роль в реализации семантики перемещения — одном из самых значительных нововведений C++11.

Итак, попробуем рассказать о ссылках в C++ максимально подробно.

Оглавление

Оглавление

1. Основы
  1.1. Определение ссылки
  1.2. Разновидности ссылок
    1.2.1. Ссылки на константу
    1.2.2. Rvalue-ссылки
    1.2.3. Ссылки на массив
    1.2.4. Ссылки на функцию
  1.3. Ссылки и указатели
    1.3.1. Взаимозаменяемость
    1.3.2. Внутреннее устройство ссылок
  1.4. Разное
    1.4.1. Полиморфизм
    1.4.2. Внешнее связывание
    1.4.3. Неполные объявления
2. Правила инициализации ссылок
  2.1. Синтаксис инициализации
  2.2. Члены класса ссылочного типа
  2.3. Категория значения
  2.4. Требования к инициализирующему выражению
  2.5. Инициализация ссылок с использованием автоопределения типа
3. Ссылки в качестве параметров и возвращаемого значения функций
  3.1. Параметры функций
    3.1.1. Специальные функции-члены и перегруженные операторы
    3.1.2. Требования к аргументам
    3.1.3. Перегрузка функций
    3.1.4. Функции с параметром типа rvalue-ссылка
  3.2. Параметры шаблонов функций
    3.2.1. Автоматический вывод аргументов шаблонов функций
    3.2.2. Явное задание аргумента шаблона функции
    3.2.3. Универсальные ссылки и rvalue-ссылки
    3.2.4. Прямая передача
    3.2.5. Перегрузка шаблонов функций
  3.3. Передача параметра по ссылке на константу vs передачи по значению
  3.4. Лямбда-выражения
    3.4.1. Автоопределение типа параметра
    3.4.2. Захват переменной по ссылке
  3.5. Возвращаемое значение функции
    3.5.1. Варианты использования
    3.5.2. Автоопределение типа возвращаемого значения
4. Висячие ссылки
  4.1. Ссылка на rvalue
  4.2. Временные объекты
  4.3. Примеры
  4.4. Стандартные контейнеры
  4.5. Другие языки
5. Ссылочные типы и шаблоны
  5.1. Ссылочные типы
  5.2. Аргументы шаблона ссылочного типа
    5.2.1. Свойства типов
    5.2.2. Свертывание ссылок
    5.2.3. Запрет на использование ссылочных типов
  5.3. Стандартный эмулятор ссылок
    5.3.1. Как устроен
    5.3.2. Использование
6. Список статей серии «C++, копаем вглубь»
7. Итоги
Список литературы

1. Основы

1.1. Определение ссылки

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

T x; T &rx = x; // rx это ссылка на x

После этого rx можно использовать в любом контексте вместо x, то есть rx становится псевдонимом x.

Инициализация ссылки обязательна, нулевые ссылки (ссылки на «ничто») не поддерживаются. Изменить переменную, на которую ссылается ссылка, невозможно — связь между ссылкой и переменной «до гробовой доски». Таким образом, ссылка является константной сущностью, хотя формально ссылочный тип не является константным.

В одной инструкции можно определить несколько ссылок, спецификатор & должен быть у каждой из них.

int x = 1, y = 2; int &rx = x, &ry = y;

Последняя инструкция эквивалентна следующим двум инструкциям:

int &rx = x; int &ry = y;

Имя типа со спецификатором & будет называться ссылочным типом. Можно объявить псевдоним для ссылочного типа.

using RT = T&;

Также можно использовать более старый способ, через typedef.

typedef T& RT;

После этого ссылки можно определить так:

int x = 1 using RI = int&; RI rx = x;

Подробнее о ссылочных типах в разделе 5.1.
Можно определить копию ссылки.

T x; T &rx = x; T &rx2 = rx;

После этого на переменную x будут ссылаться две ссылки. Других собственных операций ссылка не поддерживает, все операторы, примененные к ссылке, на самом деле применяются к переменной, на которую она ссылается. Это касается и таких операторов, как = (присваивание), & (получение адреса), sizeof, typeid. Но вот спецификатор decltype, если его применить к ссылке, дает ссылочный тип.

Остановимся подробнее на присваивании. Присваивание ссылок означает присваивание переменных, на которые ссылки ссылаются. Естественно, что тип этих переменных должен поддерживать присваивание.

int x = 1, y = 2; int &rx= x, &ry = y; rx = ry;

Последняя инструкция эквивалентна следующей:

x = y;

Ссылки rx, ry продолжат ссылаться на переменные x, y соответственно, только теперь x будет иметь значение 2. Такое поведение не вполне традиционно, в других языках происходит присваивание самих ссылок, то есть ссылка, являющаяся левым операндом, становится копией ссылки, являющейся правым операндом. (Именно так работает эмулятор ссылки — шаблон класса std::reference_wrapper<>, см. раздел 5.3.) Но в силу неизменяемости ссылок, в C++ такое невозможно.

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

int x = 1; int &rx = x; rx = 33;

Последняя инструкция эквивалентна

x = 33;

1.2. Разновидности ссылок

Выше мы определили ссылки, которые можно назвать простые ссылки. Но есть еще другие разновидности.

1.2.1. Ссылки на константу

Если T некоторый неконстантный и нессылочный тип или псевдоним, то можно определить ссылку на константу.

const T d = ini_expression; const T &rcd = d;

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

using RCT = const T&;

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

using CT = const T; using RCT = CT&;

Сами ссылки теперь можно определить так:

СT d = ini_expression; СT &rcd = d; RCT rcd2 = d;

Через ссылку на константу нельзя модифицировать объект, на которой она ссылается. Это означает, что для встроенных типов через такую ссылку запрещено присваивание, инкремент, декремент, а для пользовательских типов запрещен вызов неконстантных функций-членов.

const int d = 42; const int &rcd = d;  rcd = 43;         // ошибка

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

const int d = 42; int &rd = d;      // ошибка const int &rcd = d; int &rd2 = rcd;   // ошибка

А вот инициализировать ссылку на константу неконстантной переменной или простой ссылкой можно.

int x = 42; const int &rcx = x;   // OK int &rx = х; const int &rcx2 = rx; // OK

Напомним некоторые правила использования квалификатора const.

Если в одной инструкции объявляется несколько переменных (в том числе ссылок), то const относится ко всем переменным.

const int d1 = 1, d2 = 2; const int &rcd1 = d1, &rcd2 = d2;

Эти инструкции эквивалентны следующим инструкциям:

const int d1 = 1; const int d2 = 2; const int &rcd1 = d1; const int &rcd2 = d2;

Квалификатор const может стоять как до имени типа, так и после.

const int d = 42; const int &rcd = d;

Эти инструкции эквивалентны следующим инструкциям:

int const d = 42; int const &rcd = d;

Некоторые авторы считают последний вариант более правильным и у них есть серьезные аргументы, см. например [VJG]. В данной статье мы будем придерживаться традиционного варианта.

Нельзя быть дважды константным, компиляторы второй квалификатор const игнорируют (иногда с предупреждением).

using CT = const T; using RCT = const CT&;

Второй const игнорируется.

Ссылку на константу можно превратить в обычную ссылку с помощью оператора const_cast<>(), но это в общем случае потенциально опасное преобразование.

const int d = 42; const int &rcd = d; int &rd = const_cast<int&>(rcd); // потенциально опасно

Сделаем теперь одно терминологическое замечания. Ссылки на константу часто называют константными ссылками. Это не вполне точно, ссылки сами по себе являются константными сущностями, а вот ссылаться они могут как на константу, так и на не-константу. В случае с указателями мы должны различать эти два варианта константности, а вот в случае ссылок можно проявить некоторую терминологическую небрежность. Об этом пишет, например, Стефан Дьюхэрст [Dewhurst].

1.2.2. Rvalue-ссылки

Rvalue-ссылки — это разновидность ссылок, которая появилась в C++11. Они отличаются правилами инициализации (см. раздел 2.4) и правилами перегрузок функций с такими параметрами (см. раздел 3.1.3). Если T некоторый неконстантный и нессылочный тип или псевдоним, то rvalue-ссылка определяется так:

T &&rv = ini_expression;

То есть для их определения используется спецификатор &&, а не &.

Rvalue-ссылки представляют отдельный ссылочный тип, для него можно объявить псевдоним.

using RVT = T&&;

Компилятор различает также rvalue-ссылки на константу:

const T &&rvc = ini_expression;

но этот тип ссылок практически не используется и мы не будем его рассматривать.

Требования к ini_expression и другие подробности об rvalue-ссылках в последующих разделах.

1.2.3. Ссылки на массив

Можно определить ссылку на массив.

int a[4]; int(&ra)[4] = a;

Тип ссылки на массив включает размер массива, поэтому инициализировать нужно массивом того же размера.

int a[6]; int(&ra)[4] = a; // ошибка, размеры отличаются

Можно определить ссылку на массив констант.

const int сa[] = {1, 2, 3, 4}; const int(&rсa)[4] = ca;

Формально существуют rvalue-ссылки на массив, но они практически не используются.

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

using I4 = int[4]; I4 a; I4 &ra = a;

Можно объявить псевдоним ссылки на массив.

using RI4 = int(&)[4];

Доступ к элементу массива через ссылку осуществляется как обычно, с помощью индексатора.

int a[4]; int(&ra)[4] = a; ra[0] = 42; std::cout << ra[0];

В C++ к массивом применяется правило, называемое сведением (decay, array-to-pointer decay). (Для перевода термина «decay» еще используется слово «низведение», также можно встретить «разложение».) Суть сведения заключается в том, что почти в любом контексте идентификатор массива преобразуется к указателю на первый элемент и информация о размере теряется. Сведение происходит и при использовании массивов в качестве параметров функций. Функции

void Foo(int a[4]); void Foo(int a[]); void Foo(int *a);

не перегруженные функции, это одно и то же.

Ссылки на массив как раз и являются теми средствами, с помощью которых можно обойти сведение. Функция

void Foo(int(&a)[4]);

принимает аргументы типа int[4], массивы другого размера и указатели для нее не подходят.

Функция не может возвращать массив, а вот ссылку на массив может. Без использования псевдонимов объявление такой функции выглядит несколько пугающе:

int(&Foo(int x))[4];

Это функция, принимающая int и возвращающая ссылку на массив int[4].

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

template<typename T, std::size_t N> void Foo(T(&a)[N]);

При конкретизации такого шаблона компилятор выводит тип элементов T и размер массива N (который гарантировано больше нуля). В качестве аргументов можно использовать только массивы, указатели будут отвергнуты. Именно этот прием используется при реализации перегруженных версий std::begin(), std::end(), std::size() и других, которые позволяют трактовать обычные массивы как стандартные контейнеры.

1.2.4. Ссылки на функцию

Ссылка на функцию определяется следующим образом:

void Foo(int); void(&rf)(int) = Foo;

Для вызова функции через ссылку используется привычный синтаксис.

void Foo(int); void(&rf)(int) = Foo; rf(42); // тоже самое, что и Foo(42);

Константного варианта ссылки на функцию не существует, так как тип функции не может быть константным. Формально существуют rvalue-ссылки на функцию, но они практически не используются.

При использовании псевдонима типа функции можно получить более привычный синтаксис определения ссылки на массив.

using FI = void(int); void Foo(int); FI &rf = Foo;

Можно объявить псевдоним ссылки на функцию.

using RFI = void(&)(int);

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

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

Нельзя определить ссылку на функцию-член класса.

1.3. Ссылки и указатели

1.3.1. Взаимозаменяемость

Ссылки были добавлены в C++ в качестве более удобной альтернативы указателям, но указатели и ссылки не являются полностью взаимозаменяемыми.(Конечно, при подобной замене надо корректировать код, синтаксис доступа через ссылку и указатель разный.)

Указатели часто можно заменить ссылкой, но не всегда, так как указатель может иметь значение nullptr и это может оказаться существенным моментом в логике работы программы, когда как ссылки не могут быть нулевыми. Также нельзя создавать массивы ссылок и нет ссылочного аналога нетипизированного указателя void*. Указатели могут оказаться незаменимыми в низкоуровневых решениях, где используется арифметика указателей.

Ссылки также не всегда можно заменить указателями. В C++ классы имеют так называемые специальные функции-члены — копирующий конструктор, копирующий оператор присваивания и их перемещающие аналоги. Эти функции-члены имеют единственный параметр, который обычно имеет ссылочный тип. При перегрузке операторов также часто нельзя обойтись без параметров ссылочного типа, см. раздел 3.1.1. Эти параметры нельза заменить указателями. Rvalue-ссылки также нельзя заменить указателем.

В целом можно рекомендовать по возможности стараться использовать ссылки вместо указателей, так как указатели в значительной степени являются С-архаизмом.

1.3.2. Внутреннее устройство ссылок

Как и многие другие языки программирования, C++ скрывает внутренне устройство ссылок. Получить какую либо информацию об объекте ссылки непросто — любая операция над ссылкой означает операцию над объектом, на который она ссылается.

Достаточно традиционный взгляд — это считать ссылку «замаскированным» константным указателем. Но Страуструп и другие авторы, например Стефан Дьюхэрст [Dewhurst], считают такую точку зрения неверной и настаивают, что ссылка — это просто псевдоним переменой, на которую она ссылается. Компилятор в процессе оптимизации может вообще удалить объекты ссылок. Понятно, что в простых случаях это сделать можно (см. примеры в разделе 1.1), но как обойтись без объекта ссылки при использовании ссылок в качестве параметров и возвращаемых значений функций, членов классов и реализации полиморфизма не вполне понятно. Вот пример, который косвенно подтверждает материальность ссылок.

class X {     int &m_R; public:     X(int& r) : m_R(r){} };

По идее sizeof(X) должен давать размер объекта ссылки. Эксперименты дают ожидаемый результат — этот размер равен размеру указателя.

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

1.4. Разное

1.4.1. Полиморфизм

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

class Base { public:     virtual void Foo(); // ... }; class Derv : public Base { public:     void Foo() override; // ... };  Derv d; Base &r1 = d; r1.Foo(); // Derv::Foo() Derv &rd = d; Base &r2 = rd; r2.Foo(); // Derv::Foo()

Операторы static_cast<>() и dynamic_cast<>() можно использовать со ссылками, единственное отличие состоит в том, что если невозможно выполнить приведение dynamic_cast<>(), то при работе с указателями возвращается nullptr, а при работе со ссылками выбрасывается исключение типа std::bad_cast.

1.4.2. Внешнее связывание

Для ссылок можно реализовать внешнее связывание.

// file1.cpp extern int &ExternIntRef;  // file2.cpp int ExternInt = 125; int &ExternIntRef = ExternInt;

Скорее всего, особой пользы в этом нет, но формальная возможность есть.

1.4.3. Неполные объявления

В C++ в ряде случаев компилятору для компиляции правильного кода достаточно знать, что то или иное имя является именем какого-то пользовательского типа (класса, структуры, объединения, перечисления), а полное объявление типа не нужно. В этом случае можно использовать неполное объявление (incomplete declaration), называемое еще упреждающим или предваряющим (forward declaration). Типы с неполным объявлением называются неполными.

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

Что касается ссылок, то мы можем объявлять параметры функций, возвращаемое значение функции, члены класса, extern переменные ссылочного типа, когда тип, на который ссылается ссылка неполный. Мы можем определить ссылку на неполный тип, если она инициализируется ссылкой такого же типа, то есть допускается копирование ссылок на неполный тип.

class X; // неполное объявление class Y {     X &m_X; public:     Y(X& x) : m_X(x){ /* ... */ } // ... };

Но другие операции над ссылками невозможны без полного определения типа.

2. Правила инициализации ссылок

Ссылки должны быть обязательно инициализированы. Если ссылка объявлена глобально или в области видимости пространства имен или локально, то она должна быть инициализирована при объявлении (за исключением extern переменных). Для членов класса предназначены специальные правила инициализации, см. далее раздел 2.2.

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

2.1. Синтаксис инициализации

В C++ для инициализации переменной, в том числе и ссылки, можно использовать разные синтаксические конструкции. В данной статье мы в основном используем традиционный вариант с помощью символа =.

int x = 6; int &rx = x;

Единственный контекст, в котором такой синтаксис невозможен — это инициализация нестатического члена класса в списке инициализации конструктора, см. раздел 2.2. Обратим внимание на то, что символ = в данном случае не является оператором присваивания.

Другой вариант — это универсальная инициализация (uniform initialization), которая появилась в C++11. В этом случае используются фигурные скобки.

int x = 6; int &rx{x};

Этот вариант инициализации самый универсальный, он допустим в любом контексте.

Есть еще вариант универсальной инициализации с символом =.

int x = 6; int &rx = {x};

Но для инициализации ссылок он синтаксически избыточен. Кроме того, если определять ссылку с использованием ключевого слова auto (см. раздел 2.5), то выводимый тип будет конкретизацией шаблона std::initializer_list<>, что, скорее всего, не будет соответствовать ожиданиям программиста.

Еще один вариант — это использование круглых скобок.

int x = 6; int &rx(x);

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

class X { public:     X(); // ... };  const X &rx(X());

На первый взгляд rx — это определение переменной типа const X&, инициализированной неименованным экземпляром типа X, это полностью соответствует синтаксису C++. Но эту инструкцию также можно трактовать как объявление функции, которая возвращает const X& и имеет параметр типа указатель на функцию, которая возвращает X и не имеет параметров. В соответствии с вышеупомянутым правилом, компилятор выбирает второй вариант. Конечно, тяжелых последствий это не вызовет, так как сразу же возникнут ошибки компиляции, но потратить время на осмысление ситуации, возможно, придется. Для исправления ситуации можно, например, взять X() в дополнительные скобки.

2.2. Члены класса ссылочного типа

В классе можно объявить члены ссылочного типа. Нестатический член обычно инициализируется в списке инициализации конструктора с использованием параметров конструктора. В C++11 нестатический член можно инициализировать непосредственно при объявлении, но предложить какой-нибудь содержательный пример в данном случае сложно.

class X {     int &m_R; public:     X(int& r) : m_R(r){ /* ... */ } // ... };

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

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

Можно объявить статический член ссылочного типа. Он должен быть инициализирован при определении. В C++17 появилась возможность инициализировать такой член при объявлении, для этого он должен быть объявлен с ключевым словом inline.

сlass X { public:     static const int &H;     static inline const int &G = 32; // ... };  const int &X::H = 4;

2.3. Категория значения

В C++ каждое выражение наряду с типом имеет категорию значения (value category). (И тип и категория значения выражения известны во время компиляции.) Категория значения необходима для описания правил использования ссылок. Первоначально (в C) было только две категории значения — lvalue и rvalue. Lvalue — это именованная переменная (то, что могло находится в левой части присваивания), а rvalue — это временные, неименованные сущности (могут находится в правой части присваивания). Но в процессе развития языка определение категорий значения становится более сложным. Сейчас в C++17 имеется 5 категорий значения, подробнее см. [VJG], есть статья на Хабре, написанная igorsemenov. Для изложения представленного материала нам достаточно использовать упрощенный вариант, включающий lvalue и rvalue.

Lvalue:

  1. Именованная переменная (в том числе и rvalue-ссылка).
  2. Результат применения оператора разыменования (*).
  3. Результат применения к именованным переменным операторов доступа к членам (., ->) и индексатора.
  4. Строковый литерал.
  5. Вызов функции, которая возвращает ссылку или ссылку на константу.

Rvalue:

  1. Результат применения оператора получение адреса (&).
  2. Результат применения других операторов (за исключением lvalue п.2 и п.3).
  3. Простой литерал (42, ’X’, etc.), член перечисления.
  4. Вызов функции, которая возвращает не-ссылку.
  5. Вызов функции, которая возвращает rvalue-ссылку.

Lvalue можно еще разделить на изменяемые и неизменяемые (константные). Rvalue также можно разделить на изменяемые и неизменяемые, но неизменяемые rvalue практически не используются и мы не будем их рассматривать. Обратим внимание на пункты, начинающиеся с «Вызов функции, которая возвращает …». Под это попадают также приведения типа, в том числе и неявные.

2.4. Требования к инициализирующему выражению

Пусть T некоторый неконстантный и нессылочный тип или псевдоним.

T &r = ini_expression;

Это простая ссылка. Требования к ini_expression: lvalue типа T, T&, T&& или lvalue/rvalue любого типа, имеющего неявное преобразование к T&.

const T &r = ini_expression;

Это ссылка на константу. Требования к ini_expression: lvalue/rvalue типа T, T&, T&&, const T, const T& или любого типа, имеющего неявное преобразование к одному из этих типов.

T &&r = ini_expression;

Это rvalue-ссылка. Требования к ini_expression: rvalue типа T, T&& или lvalue/rvalue любого типа, имеющего неявное преобразования к T, T&&. Обратим внимание, что ini_expression не может быть именованной переменной ссылочного типа (в том числе и T&&), то есть прямо rvalue-ссылку скопировать нельзя. Как правильно копировать rvalue-ссылку показано далее в разделе 3.1.4

2.5. Инициализация ссылок с использованием автоопределения типа

Многие современные языки программирования со статической типизацией (то есть определяющие тип переменных на этапе компиляции) имеют возможность не указывать явно тип переменных, а предоставить вывод типа компилятору, который решает эту задачу исходя из типа инициализирующего выражения. В C++11 также появилась такая возможность, для этого используется ключевое слово auto. Но в этом случае правила вывода типа переменной не столь просты, как может показаться с первого взгляда. Ключевое слово auto может быть дополнено спецификатором ссылки и квалификатором const, что усложняет правила вывода и иногда приводит к неприятным неожиданностям. Еще следует обратить внимание на то, что в этом случае при выводе типа переменных не используются неявные преобразования типа, в том числе основанные на правилах полиморфизма. Также с использованием auto нельзя объявлять члены класса. В приводимых примерах T некоторый неконстантный и нессылочный тип или псевдоним.

auto x = ini_expression;

Тип переменной x никогда не будет выведен ссылочным или константным. Тип x выводится как T, если ini_expression имеет тип T, T&, T&&, const T, const T&, категория значения ini_expression может быть любая. В процессе инициализации вызывается копирующий или перемещающий конструктор для типа T. Если ini_expression lvalue, то будет вызван копирующий конструктор, если ini_expression rvalue, то при поддержке типом T семантики перемещения будет вызван перемещающий конструктор, иначе копирующий. В случае rvalue вызов конструктора может быть удален при оптимизации.

auto &x = ini_expression;

Тип переменной x выводится как T&, если ini_expression имеет тип T, T&, T&&. Тип x выводится как const T&, если ini_expression имеет тип const T, const T&. Если выводимый тип T&, то ini_expression должен быть lvalue.

const auto &x = ini_expression;

Тип переменной x выводится как const T&, если ini_expression имеет тип T, T&, T&&, const T, const T&, категория значения ini_expression может быть любая.

auto &&x = ini_expression;

Этот тип ссылки называется универсальной ссылкой (univercal reference), и имеет довольно специфические правила вывода, выводимый тип зависит от категории значения ini_expression. Тип переменной x выводится как T&, если ini_expression является lvalue и имеет тип T, T&, T&&. Тип переменной x выводится как const T&, если ini_expression является lvalue и имеет тип const T, const T&. Тип переменной x выводится как T&&, если ini_expression является rvalue и имеет тип T, T&, T&&. В C++17 этот тип ссылки стали называть передаваемой ссылкой (forwarding reference), о причинах рассказано далее в разделе 3.2.4.

Особо следует отметить случай, когда ini_expression является массивом или функцией. В этом случае в определении

auto x = ini_expression;

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

3. Ссылки в качестве параметров и возвращаемого значения функций

На самом деле ссылочные типы в основном используются в качестве типа параметров и возвращаемого значения функций, а не для создания переменных.

3.1. Параметры функций

В этом случае ссылки обеспечивают ряд преимуществ.

  1. Затраты на передачу параметра постоянны и не зависят от типа, на который ссылается ссылка (они эквиваленты затратам на передачу указателя).
  2. Позволяют модифицировать объект, на который ссылается параметр, то есть превращать параметр в выходной.
  3. Позволяют запретить модифицировать объект, на который ссылается параметр.
  4. Обеспечивают реализацию семантики перемещения.
  5. Передача ссылки по стеку вызовов не приводит к появлению висячих ссылок.
  6. Поддерживают полиморфизм.

3.1.1. Специальные функции-члены и перегруженные операторы

В C++ классы имеют так называемые специальные функции-члены — копирующий конструктор, копирующий оператор присваивания и их перемещающие аналоги. Эти функции-члены имеют единственный параметр, который обычно имеет ссылочный тип. При перегрузке операторов также часто нельзя обойтись без параметров ссылочного типа.

class X { public:     X(const X& src); // копирующий конструктор     X& operator=(const X& src); // оператор копирующего                                 // присваивания     X(X&& src) noexcept; // перемещающий конструктор     X& operator=(X&& src) noexcept;// оператор перемещающего                                    // присваивания // ... };  X operator+(const X& lh, const X& rh); // перегруженный оператор +

Для копирующего конструктора и перемещающих операций тип параметра изменить нельзя. При перегрузке операторов (в том числе и оператора копирующего присваивания) передачу параметра по ссылке иногда можно заменить передачей по значению, см. раздел 3.3.

3.1.2. Требования к аргументам

Рассмотрим особенности использования параметров функций ссылочного типа. В приводимых примерах T некоторый неконстантный и нессылочный тип.

void Foo(T x);

Это передачу параметра по значению. Подробнее см. раздел 3.3. В ряде случаев мы должны сравнивать передачу параметра по значению и передачу параметра по ссылке.

void Foo(T& x);

Параметр — простая ссылка. Требования к аргументу: lvalue типа T, T&, T&& или lvalue/rvalue любого типа, который имеет неявное преобразование к T&. В этом случае мы имеем возможность модифицировать аргумент, то есть x может быть выходным параметром.

void Foo(const T& x);

Параметр — ссылка на константу. Требования к аргументу: lvalue/rvalue типа T, T&, T&&, const T, const T& или любого типа, имеющего неявное преобразование к одному из этих типов. В этом случае мы не имеем возможность модифицировать аргумент.

void Foo(T&& x);

Параметр — rvalue-ссылка. Требования к аргументу: rvalue типа T, T&& или lvalue/ rvalue любого типа, который имеет неявное преобразование к T, T&&. Этот вариант используется для реализации семантики перемещения. В классе, поддерживающем перемещение, должен быть определен перемещающий конструктор с параметром типа rvalue-ссылка и оператор перемещающего присваивания с таким же параметром.

class X { public:     X(X&& src) noexcept;     X& operator=(X&& src) noexcept; // ... };

Эти функции-члены и выполняют в конечном итоге перемещение. В ряде случаев компилятор сам генерирует перемещающий конструктор и оператор перемещающего присваивания, подробности см. [Meyers]. Использование noexcept не является строго обязательным, но крайне желательным, иначе в стандартной библиотеке в некоторых случаях перемещение будет заменено на копирование, подробности см. [Meyers].

Ключевой момент концепции семантики перемещения заключается в том, что источником перемещения является rvalue и, таким образом, после выполнения перемещения этот объект будет недоступен и не надо беспокоиться о случайном доступе к «опустошенному» объекту. (Возможно принудительное приведение lvalue к rvalue (см. раздел 3.1.4 ), но в этом случае программист уже сам отвечает за недопущение некорректных операций.)

3.1.3. Перегрузка функций

Перегрузка (overloading) — это возможность одновременно использовать несколько функций или шаблонов функций с одним именем. Компилятор различает их благодаря тому, что они имеют разный набор параметров. В точки вызова компилятор анализирует типы аргументов и определяет, какая конкретно функция должна быть вызвана. Эта процедура называется разрешением перегрузки. Разрешение перегрузки может завершиться неудачей, то есть компилятор может не отдать предпочтение ни одной из функций, в этом случае говорят, что вызов неоднозначный (ambigious). Более подробно перегрузка обсуждается в одной из предыдущих статей серии.

Правила разрешения перегрузки имеют важное значение: для того, чтобы реализовать свои замыслы, программист должен четко понимать какая из перегруженных функций будет вызвана в том или ином контексте. В частности семантика перемещения базируется на правилах перегрузки функций, имеющих параметры типа rvalue-ссылка, и неправильно понимание правил перегрузки может привести к тому, что перемещение «молча» будет заменено копированием. Правила перегрузка функций, имеющих параметры ссылочного типа, можно считать расширением правил, изложенных в предыдущем разделе, так как они определяют выбор между несколькими допустимыми вариантами.

Пусть функции перегружены следующим образом:

void Foo(T& x); void Foo(const T& x);

В этом случае для неконстантных lvalue-аргументов будет выбрана первая функция (хотя вторая также допустима), для константных lvalue-аргументов и rvalue-аргументов вторая (первая недопустима).

Пусть функции перегружены следующим образом:

void Foo(T& x); void Foo(T x);

Здесь для константных lvalue-аргументов и rvalue-аргументов будет выбрана вторая функция (первая недопустима), а вот для неконстантных lvalue-аргументов разрешение перегрузки завершится неудачей (хотя обе функции допустимы), то есть для этого варианта перегруженных функций первая функция никогда не будет выбрана.

Пусть функции перегружены следующим образом:

void Foo(const T& x); void Foo(T x);

Для любых аргументов разрешение перегрузки завершится неудачей (хотя обе функции допустимы).

Пусть функции перегружены следующим образом:

void Foo(const T& x); void Foo(T&& x);

В этом случае первая функция будет выбрана для lvalue-аргументов (вторая недопустима), а вторая для rvalue-аргументов (хотя первая допустима). Это правило является ключевым при реализации семантики перемещения, именно оно используется при выборе между копирующим и перемещающим конструктором (а также соответствующими операторами присваивания).

class X { public:     X(const X& src);     // копирующий конструктор     X(X&& src) noexcept; // перемещающий конструктор // ... };

Пусть функции перегружены следующим образом:

void Foo(T& x); void Foo(T&& x);

В этом случае первая функция будет выбрана для неконстантных lvalue-аргументов (вторая недопустима), вторая для rvalue-аргументов (первая недопустима), а для константных lvalue-аргументов обе функции недопустимы и, соответственно, разрешение перегрузки завершится неудачей.

Пусть функции перегружены следующим образом:

void Foo(T x); void Foo(T&& x);

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

Отметим также появившееся в С++11 ссылочные квалификаторы для нестатических функций-членов. Они позволяют перегружать по категории значения скрытого параметра this.

class X { public:     X();     void Foo() &;  // this указывает на lvalue     void Foo() &&; // this указывает на rvalue // ... };  X x; x.Foo();   // X::Foo() & X().Foo(); // X::Foo() &&

3.1.4. Функции с параметром типа rvalue-ссылка

Рассмотрим случай, когда у нас есть функция с параметром типа rvalue-ссылка. Такая функция принимает только rvalue-аргументы. Пусть теперь этот параметр мы просто должны передать другой функции с параметром типа rvalue-ссылка. В этом случае необходимо учитывать, что сам параметр будет lvalue и поэтому для корректной передачи такой параметр необходимо пропустить через преобразование типа static_cast<T&&>() или вызов стандартной функции std::move(), которые превращают lvalue в rvalue.

class X;  void FooInt(X&& x); void Foo(X&& x) { // x это lvalue, а std::move(x) это rvalue     FooInt(std::move(x)); // ... }

Если этого не сделать, то будет либо ошибка, либо, если есть перегруженная функция c параметром типа X, X&, const X&, то будет выбрана она (в частности перемещение может быть заменено на копирование, см. раздел 3.1.3). Таким образом, без этого преобразования семантика перемещения в дальнейшем не будет работать. Такие ошибки опасны тем, что их можно долго не замечать.

Обратим внимание на немного сбивающее с толка название std::move(). Реально эта функция ничего не перемещает, это приведение типа, которое превращает lvalue в rvalue и использовать ее надо только так, как показано в примере — ее вызов должен быть аргументом функции, с параметром rvalue-ссылка. Реальное перемещение делает перемещающий конструктор.

3.2. Параметры шаблонов функций

3.2.1. Автоматический вывод аргументов шаблонов функций

Аргументы шаблона функции могут выводиться компилятором автоматически, основываясь на типе аргумента вызова. Это наиболее распространенный вариант использования шаблонов функций. Если аргумент шаблона выводится автоматически, то правила вывода практически полностью совпадают с правилами вывода для объявлений с помощью ключевого слова auto. При описании аргумента будем считать T неконстантным и нессылочным типом или псевдонимом.

template<typename T> void Foo(T x);

Тип аргумента шаблона выводится как T, если аргумент имеет тип T, T&, T&&, const T, const T&, тип параметра x будет T, категория значения аргумента может быть любая. Таким образом, тип T никогда не будет выведен ссылочным или константным. Здесь мы имеем передачу параметра по значению.

template<typename T> void Foo(T& x);

Тип аргумента шаблона выводится как T, если аргумент имеет тип T, T&, T&&, тип параметра x будет T&, аргумент должен быть lvalue. Тип аргумента шаблона выводится как const T, если аргумент имеет тип const T, const T&, тип параметра x будет const T&, категория значения аргумента может быть любая.

template<typename T> void Foo(const T& x);

Тип аргумента шаблона выводится как T, если аргумент имеет тип T, T&, T&&, const T, const T&, тип параметра x будет const T&, категория значения аргумента может быть любая.

template<typename T> void Foo(T&& x);

Это универсальная ссылка. Аргумента шаблона выводится как T&, если аргумент lvalue и имеет тип T, T&, T&&, тип параметра x будет также T&. Аргумента шаблона выводится как const T&, если аргумент lvalue и имеет тип const T, const T&, тип параметра x будет также const T&. Тип аргумента шаблона выводится как T, если аргумент rvalue и имеет тип T, T&, T&&, тип параметра x будет T&&.

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

template<typename T> void Foo(T x);

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

3.2.2. Явное задание аргумента шаблона функции

Аргумент шаблона функции может быть указан явно. Иногда это вынужденное решение, когда автоматический вывод невозможен (например для типа возвращаемого значения) или не дает нужный результат (например для ссылочных типов). В этом случае механизм вывода аргументов шаблона не используется и мы фактически имеем дело с нешаблонной функцией. В частности, явное задание аргумента используется в рассматриваемом далее шаблоне функции std::forward<>().

Пусть у нас параметр функции имеет тип ссылки на параметр шаблона. В этом случае, если явно заданный аргумент шаблона будет иметь ссылочный тип, то мы получим, что параметр функции будет иметь тип ссылка на ссылку. В C++ такие типы запрещены, поэтому в этой ситуации выполняется операция под названием свертывание ссылок (reference collapsing), в результате чего тип параметра функции будет ссылка или rvalue ссылка. Подробнее свертывание ссылок рассмотрено в раздел 5.2.2, простые примеры будут в следующем разделе.

3.2.3. Универсальные ссылки и rvalue-ссылки

Универсальная ссылка и rvalue-ссылка объявляются одинаково, с помощью спецификатора &&, поэтому важно четко понимать, с каким вариантом мы имеем дело в том или ином случае.

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

  1. Наличие шаблона функции с типовым параметром (обозначим его через T).
  2. Параметр функции объявлен как T&&.
  3. Аргумент шаблона выводится автоматически, исходя из типа аргумента вызова функции.

Если аргумент шаблона задается явно и параметр шаблона функции объявлен как T&&, то в случае аргумента шаблона ссылочного типа применяется свертывание ссылок (см. раздел 5.2.2) и параметр конкретизированной функции превратится в обычную ссылку или rvalue-ссылку. Если аргумент шаблона нессылочного типа, то параметр будет rvalue-ссылка.

Рассмотрим примеры.

class X { public:     X(); // ... };  X x; // x это lvalue

Рассмотрим несколько вариантов использования x в качестве аргумента при вызове функции.

void F(X&& x);  F(x); // ошибка

В данном случае у нас обычная функция (нарушено условие 1), параметр имеет тип rvalue-ссылка, lvalue-аргумент не подходит.

template<typename T> void Foo(T&& x)  Foo(x); // OK

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

Foo<X>(x); // ошибка

Аргумент шаблона задается явно, параметр в данном случае имеет тип rvalue-ссылка, lvalue-аргумент не подходит.

Foo<X&>(x); // OK

Аргумент шаблона задается явно и имеет ссылочный тип, следовательно выполняется свертывание ссылок (X& && -> X&). Параметр будет обычной ссылкой, поэтому можно использовать lvalue-аргумент.

template<typename T> class W { public:     W();     void Foo1(T&& x);     template<typename U>     void Foo2(U&& x); // ... };  W<X> wx; wx.Foo1(x);  // ошибка

Тип параметра функции-члена Foo1() определяется явно, при конкретизации шаблона класса W, параметр имеет тип rvalue-ссылка, lvalue-аргумент не подходит.

W<X&> wrx; wrx.Foo1(x); // OK

Тип параметра функции-члена Foo1() определяется явно, при конкретизации шаблона класса W, аргумент шаблона класса имеет ссылочный тип, следовательно выполняется свертывание ссылок (X& && -> X&). Параметр будет обычной ссылкой, поэтому можно использовать lvalue-аргумент.

W<X> wx; wx.Foo2(x);

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

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

Универсальные ссылки также используются при выводе типа переменных, объявленных с помощью auto &&. Это происходит при объявлении переменных (см. раздел 2.5) и параметров лямбда-выражений (см. раздел 3.4.1).

3.2.4. Прямая передача

Теперь рассмотрим ситуацию, когда параметр функции, являющейся универсальной ссылкой, надо передать в другую функцию. Параметр функции всегда будет lvalue и для того, чтобы его корректно передать другой функции, мы должны его преобразовать в rvalue, но только тогда, когда аргумент является rvalue, то есть мы должны сохранить категорию значения аргумента. Если этого не сделать, то могут возникнуть проблемы, описанные в разделе 3.1.4, то есть по существу будет отключена семантика перемещения. В силу того, что аргумент шаблона имеет разный тип в зависимости от категории значения аргумента вызова, задача эта решаема и для этого служит стандартный шаблон функции std::forward<>(). Его надо конкретизировать параметром шаблона и параметр пропустить через вызов этой функции.

class X { public:     X(); // ... }; void FooInt(const X& x); // для lvalue void FooInt(X&& x);      // для rvalue  template<typename T> void Foo(T&& x) { // x это lvalue, а std::forward<T>(x) это // lvalue, если аргумент lvalue и rvalue, если аргумент rvalue     FooInt(std::forward<T>(x)); // ... } X x; Foo(x);   // FooInt(const X& x), lvalue аргумент Foo(X()); // FooInt(X&& x), rvalue аргумент

Эта схема передачи параметра называется прямой (иногда идеальной) передачей (perfect forwarding). Теперь понятно, почему универсальную ссылку стали называть передаваемой (forwarding reference). Универсальные ссылки и прямая передача являются довольно сложной темой со своими «подводными камнями». У Скотта Мейерса [Meyers] можно найти много важных и интересных подробностей на этот счет.

Опять же обратим внимание на то, что шаблон std::forward<>() — это преобразование типа, ничего больше он не делает. Его задача — обеспечить корректную работу правил вызова и перегрузки функций, с учетом категории значения аргумента.

3.2.5. Перегрузка шаблонов функций

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

Правила разрешения можно описать так: рассматриваются конкретизации шаблонов и нешаблонных функций и для них применяются правила из раздела 3.1.3. Когда обе функции одинаковы, применяется следующее правило: если одна из них нешаблонная функция, то будет выбрана она, если обе являются конкретизациями шаблонов, то выбирается конкретизация более специализированного шаблона, если нельзя выбрать более специализированный шаблон, то разрешение перегрузки завершается неудачей. Шаблон с универсальной ссылкой будет считаться менее специализированным, чем шаблон с параметром типа ссылка или ссылка на константу. Отметим, что если какой-либо шаблон не конкретизируется, то он просто исключается из разрешения перегрузки (принцип SFINAE). Приведем примеры.

template<typename T> void Foo(T&& x); template<typename T> void Foo(T& x);

Для lvalue-аргументов конкретизации обоих шаблонов одинаковы, но будет выбран второй шаблон как более специализированный. Для rvalue-аргументов конкретизации разные и по правилам раздела 3.1.3 будет выбран первый шаблон.

template<typename T> void Foo(T&& x); class X; void Foo(X&& x);

Для rvalue-аргументов типа X, X&& конкретизации обоих шаблонов одинаковы и будет выбрана вторая функция как нешаблонная, для остальных аргументов первый шаблон.

Перегрузка шаблонов функций и нешаблонных функций скрывает в себе некоторые потенциальные проблемы. Дело в том, что шаблоны часто оказываются жадными (greedy), они выбираются при разрешении перегрузки чаще, чем, возможно, ожидает программист, так как их конкретизации почти всегда дают точное соответствие для аргументов. Шаблоны функций, которые участвуют в перегрузке, могут, по существу, отключить неявные преобразования аргументов для нешаблонных функций. Подробнее эта тема обсуждается в одной из предыдущих статей серии.

3.3. Передача параметра по ссылке на константу vs передачи по значению

Передача параметра по ссылке на константу и по значению в ряде случаев выступают как конкуренты.

void Foo(const T& x); // передача по ссылке на константу void Foo(T x);        // передача по значению

Рассмотрим особенности каждого варианта.

Эти варианты не могут быть перегружены (см. раздел 3.1.3), то есть программист заранее должен выбрать один из них. В любом из них аргумент может быть типа T, T&, T&&, const T, const T& или любого типа, имеющего неявное преобразование к какому-то из этих типов, категория значения аргумента может быть любая. Оба варианта гарантируют неизменяемость аргумента.

Рассмотрим теперь требования к типу T и накладные расходы при передаче параметра. При передаче по ссылке на константу особых требований к типу T нет, копируется ссылка, затраты постоянны и совпадают с затратами по копированию указателя. При передаче по значению для lvalue-аргументов вызывается копирующий конструктор, а для rvalue-аргументов вызывается перемещающий конструктор, если тип T поддерживает семантику перемещение, и копирующий конструктор в противном случае. В C++17 для rvalue-аргументов при передаче по значению в ряде случаев не требуется наличия копирующего или перемещающего конструктора, так как оптимизации, удаляющие вызов конструктора, внесены в стандарт и наличие соответствующего конструктора уже не требуется. В предыдущих версиях C++ требовалось наличие конструктора, даже когда он удалялся при оптимизации.

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

Передача по ссылке на константу поддерживает полиморфизм, аргумент может иметь тип производный по отношению к типу параметра, но параметр функции получит «правильный» указатель на таблицу виртуальных функций, такой как у аргумента. При передаче по значению происходит так называемая «срезка» (slicing) и вся информация о производном типе теряется.

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

При перегрузке операторов вместо передачи параметра по ссылке на константу иногда можно использовать передачу по значению. Один из известных примеров, где подобное решение целесообразно — это оператор присваивания, реализованный с помощью идиомы «копирование и обмен».

class X { public:     X& operator=(X src); // ... };

Но вот как раз с таким оператором присваивания есть одна потенциальная проблема: вместе с ним нельзя будет использовать оператор перемещающего присваивания, так как для них разрешение перегрузки окончится неудачей для rvalue-аргументов, см. правила перегрузки в разделе 3.1.3.

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

Интересные и не совсем традиционные размышления по поводу этих вариантов можно найти у Скотта Мейерса [Meyers].

3.4. Лямбда-выражения

3.4.1. Автоопределение типа параметра

Лямбда-выражения до C++20 не могли быть шаблонами, но в С++14 появилась возможность для типа параметров использовать ключевое слово auto, при этом можно использовать спецификатор ссылки и квалификатор const. Это частично компенсировало отсутствие шаблонов (вывод типа auto и вывод типа аргумента шаблона это практически одно и то же). Например, параметр типа универсальная ссылка в лямбда-выражении можно объявить следующим образом:

[](auto&& x){ /* ... */ }

Но тут возникает вопрос — что делать, если надо реализовать прямую передачу? Для прямой передачи мы должны шаблон функции std::forward<>() конкретизировать аргументом шаблона, а его в данном случае нет. Оказывается, вместо аргумента шаблона можно использовать decltype(x). Для rvalue-аргументов это будет другой тип по сравнению с типом, выводимым для шаблонов с универсальной ссылкой, но, несмотря на это, std::forward<>() будет работать так, как надо для прямой передачи. Таким образом, в качестве передаваемого дальше аргумента надо использовать выражение std::forward<decltype(x)>(x). Детали можно найти у Скотта Мейерса [Meyers].

3.4.2. Захват переменной по ссылке

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

int callCntr = 0; auto g = [&callCntr](){ ++callCntr; }; g();

В этом примере переменная callCntr используется в качестве счетчика вызовов.

Захват по ссылке потенциально может привести к появлению висячей ссылки (см. раздел 4), так как замыкания (в нашем примере это g) можно копировать и потенциально копия может иметь время жизни больше, чем захваченная переменная.

3.5. Возвращаемое значение функции

3.5.1. Варианты использования

Использование ссылок в качестве возвращаемых значений функций таит в себе определенную опасность, могут появиться висячие ссылки, см. раздел 4. Но, не смотря на это, такой прием используется достаточно широко, в том числе и в стандартной библиотеке.
В качестве первого примера рассмотрим итераторы. В стандартном интерфейсе итератора перегруженный оператор * (разыменование) обычно возвращает ссылку на объект, хранимый в контейнере. У некоторых стандартных контейнеров есть еще специальные функции-члены, например, индексатор, front(), back(), которые возвращают ссылку на объект, хранимый в контейнере.
Вызов функции, которая возвращает ссылку, может находиться в левой части оператора присваивания. Это делает код более компактным и читабельным и позволяет использовать перегруженные операторы для пользовательских типов таким же образом, как и для встроенных типов.

std::vector<int> v(2); v.front() = 31; v[1] = 41;

При перегрузке оператора присваивания (и составных операторов присваивания: +=, etc.) возвращаемое значение должно быть ссылкой на результат операции, это позволяет строить цепочку присваиваний.

x = y = z;

Еще один пример использования ссылок в качестве возвращаемого значения — это потоки ввода/вывода, где перегруженные операторы >> и << должны возвращать ссылку на поток, что дает возможность строить цепочку операций.

int x, y; std::cout << "x=" << x << ", y=" << y << '\n';

Накладные расходы, связанные возвращаемым значением ссылочного типа, невелики, они совпадают с соответствующими расходами для указателя. Они могут быть еще меньше в результате применения оптимизаций (RVO, etc).

3.5.2. Автоопределение типа возвращаемого значения

В C++14 появилась возможность не указывать явно, а выводить тип возвращаемого значения функции. Для этого в качестве типа возвращаемого значения указывается auto, при этом можно использовать спецификатор ссылки и квалификатор const. Правила вывода типа те же, что и при инициализации переменных, объявленных с помощью auto, см. раздел 2.5.

В качестве типа возвращаемого значения можно также указать decltype(auto). В этом случае тип возвращаемого значения выводится как decltype(return_expression). То есть, если return_expression будет иметь ссылочный тип, то таким же будет и тип возвращаемого значения.

Автоопределение типа возвращаемого значения используется в основном в шаблонах функций.

4. Висячие ссылки

Для любой ссылки в широком смысле существует проблема висячей ссылки (dangling reference). Она возникает, когда объект, на который ссылается ссылка, удаляется или перемещается, а ссылка про это «ничего не знает». В этом случае использование ссылки приводит к так называемому неопределенному поведению, то есть может произойти все, что угодно — аварийное завершение программы, неверный, но правдоподобный результат и другие неприятные вещи.

В C++ в ряде случаев компилятор гарантирует отсутствие висячих ссылок, но в общем случае программисту самому приходиться следить, чтобы висячие ссылки не появлялись.

Потенциальные проблемы появляются, когда ссылка копируется и копия имеет время жизни больше, чем у исходной ссылки. Такая ситуация может возникнуть при использовании функции, которая возвращает ссылку. Еще одна ситуация, когда мы получаем копию ссылки с потенциально большим временем жизни — это использование членов класса ссылочного типа.

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

4.1. Ссылка на rvalue

Как мы видели выше, ссылка на константу и rvalue-ссылка может быть инициализирована rvalue. Спрашивается, а на что она тогда ссылается? Компилятор в этом случае реализует механизм под названием временная материализация (temporary materialization) — создается скрытая переменная, которая инициализируется этим rvalue, и ссылка будет ссылаться на эту переменную. И самое важное, компилятор обеспечивает время жизни этой переменной не меньше, чем время жизни ссылки, поэтому такая ссылка никогда не станет висячей. Следующий странноватый на первый взгляд код является совершенно корректным.

int &&rr = 7; rr = 8;

Литерал 7 — это rvalue, значить происходит временная материализация и во второй инструкции просто меняется значение соответствующей скрытой переменной.

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

4.2. Временные объекты

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

4.3. Примеры

Можно, конечно, висячую ссылку создать как-нибудь так:

int &dx = *new int(32); delete &dx; // dx — висячая ссылка

Но подобный код встречается, наверное, очень редко. Чаще всего проблемы возникают, когда объект удаляется неявно. Рассмотрим насколько типичных сценариев.

Одна из самых грубых ошибок — это возврат из функции ссылки на локальный объект.

class X { public:     X(); // ...  };  X& Foo() {     X x; // ...     return x; }  const X& Foo2() {     const X &ret = X(); // ...     return ret; }

Подобный код гарантирует висячую ссылку. (Правда иногда может спасти inline подстановка.) Компилятор выдает предупреждение, но не ошибку.

Рассмотрим теперь функцию:

const X& Foo(const X& x) { // ...     return x; }

Если при вызове этой функции используется lvalue-аргумент, то гарантируемых проблем не возникает, время жизни x будет определяться контекстом вызова, но в случае rvalue-аргумента время жизни x будет тело функции и после вызова этой функции возвращаемая ссылка будет ссылаться на удаленный объект. Спасти ситуацию может inline подстановка или, если вызов этой функции будет инициализировать значение X, а не ссылку, тогда деструктор X будет вызван после копирования.

Интересно, что подобным образом реализованы некоторые стандартные функции, например:

// https://en.cppreference.com/w/cpp/algorithm/max // header <algorithm> // namespace std  template<class T> const T& max(const T& a, const T& b);

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

Рассмотрим теперь класс, имеющий ссылку в качестве члена. Такой член должен быть инициализирован в списке инициализации конструктора.

class X; class Y {     const X &m_X; // ... public:     Y(const X& x) : m_X(x){ /* ... */} // ... };

Здесь та же проблема, что и в предыдущем примере. Если в конструкторе используется lvalue-аргумент, то все может работать, но если аргумент является rvalue, то m_X гарантируемо будет висячей ссылкой. В этом случае можно подстраховаться и запретить инициализацию экземпляра Y с помощью rvalue, для этого надо объявить конструктор с параметром типа rvalue-ссылка удаленным.

Y(X&&) = delete;

Рассмотрим еще один пример.

class X {     int m_Value; public:     X(int x) :  m_Value(x) {}     const int& Value() const { return m_Value; } };

Рассмотрим первый вариант использования этого класса.

const int &rxv = X(32).Value();

Ссылка на константу rxv инициализируется вызовом функциии, которая возвращает ссылку на константу, а это lvalue (см. раздел 2.3), поэтому временной материализации не будет. Но эта функция является функцией-членом, которая возвращает ссылку на подобъект временного объекта типа X. В соответствии с разделом 4.2 этот временный объект будет удален сразу после того, как ссылка rxv будет инициализирована. Получаем висячую ссылку.

Этот пример показывает потенциально опасную ситуацию — «превращение» rvalue в lvalue с помощью вызова функции, которая возвращает ссылку, и как следствие появления висячей ссылки. Ситуация будет еще более опасной, когда класс имеет неявное преобразование к ссылке, применение неявного преобразования — это по существу вызов функции, но происходит это неявно.

Перепишем предыдущий код следующим образом:

const X &rx = X(32); const int &rxv = rx.Value();

Ссылка на константу rx инициализируется временным объектом типа X, а это rvalue, поэтому происходит временная материализация и rxv будет ссылаться на подобъект «живого» объекта и, таким образом, rxv не будет висячей ссылкой во время жизни rx.

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

void Foo(const int& rr); Foo(X(32).Value());

В этом примере временной материализации не будет по той же причине, что и в первом варианте, но временный экземпляр X будет удален, только после того, как Foo() вернет управление (см. раздел 4.2) и, таким образом, в теле функции Foo() ссылка rr не будет висячей.

В C++11 можно запретить вызов нестатической функции-члена для rvalue:

const int& Value() const & { return m_Value; } const int& Value() const && = delete;

Здесь мы использовали так называемые ссылочные квалификаторы для нестатических функций-членов. Они позволяют перегружать по категории значения скрытого параметра this (см. раздел 3.1.3).

4.4. Стандартные контейнеры

Стандартный доступ к элементам контейнера осуществляется через итератор. В интерфейсе итератора есть перегруженный оператор * (разыменование), который обычно возвращает ссылку на объект, хранимый в контейнере. Если после получения такой ссылки происходит какая-то операция с контейнером, то эта ссылка может оказаться висячей. Понятно, что для любого контейнера вызов clear() гарантирует, что все ранее полученные ссылки становятся висячими. Вот менее очевидный пример — при добавлении элемента в экземпляр std::vector<> может произойти выделение нового буфера и копировании или перемещение всех старых данных в новый буфер, после чего все ранее полученные ссылки становятся висячими. В документации по стандартной библиотеке можно найти информацию о том, при каких операциях с контейнером гарантируется, что ранее полученные итераторы не станут недействительными.

В стандартных контейнерах также есть другие функции-члены (индексатор, front(), back(), etc.), возвращающие ссылки на элементы, хранимые в контейнере, эти ссылки также могут стать висячими.

4.5. Другие языки

Многие языки программирования озабочены проблемой висячих ссылок. В языках, использующих сборку мусора (C#, Java и многие другие), эта проблема решается так: объект, контролируемый сборщиком мусора, может быть удален только после того, как на него не останется ссылок, а при перемещении объекта ссылки автоматически корректируются.

Другой пример — это Rust. Одна из рекламируемых особенностей этого языка является более сложная система отслеживания жизненного цикла ссылок и, возможно, некоторые из описанных выше проблем были бы обнаружены на стадии компиляции.

5. Ссылочные типы и шаблоны

5.1. Ссылочные типы

Если T некоторый нессылочный тип или псевдоним, то T& будет соответствующим ссылочным типом. Можно объявить псевдоним ссылочного типа

using RT = T&;

или с использованием традиционного typedef

typedef T& RT;

Если не использовать псевдонимы, то ссылочные типы для массивов и функций надо объявлять несколько по другому, см. разделы 1.2.3, 1.2.4.

Ссылки на константу представляют отдельный ссылочный тип:

using RCT = const T&;

Если T константный тип, то в этом объявлении const игнорируется. (Нельзя быть дважды константным.)

Rvalue-ссылки также представляют отдельные ссылочные типы:

using RVT = T&&;

Ссылочные типы являются практически полностью скрытыми, в том смысле, что любой запрос по поводу этого типа (например sizeof или typeid) будет переадресован к типу, на который этот ссылочный тип ссылается. Размер самой ссылки можно узнать только косвенно, см. раздел 1.3.2. Из-за этих особенностей у ссылочных типов имеются ряд ограничений.

Нельзя объявить указатель на ссылку.

T x; T &rx = x; using RT = T&; RT *prx = rx;    // ошибка using PRT = RT*; // ошибка

Если бы и был тип указателя на ссылку, то мы не могли бы инициализировать экземпляр такого типа, так как оператор & (получение адреса), примененный к ссылке, возвращает указатель на объект, на который ссылка ссылается.

Но можно объявить ссылку на указатель.

T x; T *px = &x; using PT = T*; PT &rpx = px;  using RPT = PT&;

Нельзя определить ссылку на ссылку.

T x; T &rx = x; using RT = T&; RT &rrx = rx; // ошибка

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

using RT = T&; using RRT = RT&; // OK

На самом деле типом RRT будет T&, почему это так будет объяснено далее в разделе 5.2.2.

Нельзя объявить массив ссылок. Если мы попробуем как-нибудь так

int x = 1, y = 2; int &ra[] = {x, y}; // ошибка

то компилятор выдаст ошибку. Это вытекает из предыдущих ограничений — тип, используемый для определения массива, должен иметь соответствующий тип указателя. Кроме того, оператор sizeof, примененный к элементу массива, должен давать размер элемента, а в случае массива ссылок он будет давать не размер самой ссылки, а размер объекта, на который эта ссылка ссылается.

Нельзя объявить ссылку на void.

using RVOID = void&; // ошибка

5.2. Аргументы шаблона ссылочного типа

В общем случае запрета на использование ссылочного типа в качестве аргумента шаблона нет, но в силу его особенностей тот или иной шаблон может не конкретизироваться с такими аргументами или, что еще опаснее, конкретизироваться, но работать неправильно. Другая проблема — это то, что при автоматическом выводе аргумента шаблона ссылочные типы выводятся только для параметров с универсальными ссылками. Это вынуждает использовать явное задание аргумента шаблона или использовать некоторые специальные «хитрости» (см. раздел 5.3.2).

5.2.1. Свойства типов

При разработке шаблонов широко используются специальные стандартные шаблоны, которые называются свойствами типов (заголовочный файл <type_traits>). Среди них есть несколько, предназначенных работать со ссылочными типами. Прежде всего, это std::is_reference<>, его статический член value будет иметь значение true, в случае аргументов шаблона ссылочного типа. (На самом деле есть еще свойства типа для более тонкой проверки: std::is_lvalue_reference<>, std::is_rvalue_reference<>.) Отметим, что для остальных свойств типов из этой группы (std::is_const<>, std::is_integral<>, etc.) это значение будет false и не будет зависеть от типа, на который ссылочный тип ссылается. Также можно использовать шаблон std::remove_reference<>, который превращает ссылочный тип в соответствующий нессылочный (типовой член шаблона type). Шаблон std::decay<> также снимает ссылочность, но выполняет еще и другие операции над типом.

5.2.2. Свертывание ссылок

Как уже упоминалось выше, ссылки на ссылку не существует, но при использовании аргументов шаблона ссылочного типа в ряде контекстов могут появляться конструкции, которые по правилам C++ интерпретируются как ссылки на ссылку. В этом случае применяется особое правило, которое называется свертывание ссылок (reference collapsing). В результате такая конструкция интерпретируется как ссылка или rvalue-ссылка на нессылочный тип. Правило простое — если обе ссылки являются rvalue-ссылками, то результирующая ссылка также будет rvalue-ссылка, в противном случае результирующая ссылка будет обычная ссылка. (На самом деле правило немного сложнее, нужно еще учитывать константность, см. [VJG].)

Первый пример — это вывод типа параметров функций при явном задании аргумента шаблона (см. также раздел 3.2.3).

template<typename T> class W { public:     W() = default;     void Foo(T&& x); // ... };  class X { /* ... */ };  W<X> wx;     // void Foo(X&&); // нет свертывания W<X&> wrx;   // void Foo(X&);  // X& && -> X& W<X&&> wrvx; // void Foo(X&&); // X&& && -> X&&

Другой пример — это объявление псевдонимов.

using RI = int&; using RRI = RI&;  // int& & -> int&  using RI = int&; using RRI = RI&&; // int& && -> int&  using RI = int&&; using RRI = RI&&; // int&& && -> int&&

Аналогичным образом работают правила объявления псевдонимов с помощью typedef.

Свертывание ссылок появилось в C++03 и было доработано в C++11, подробнее см. [VJG], [Meyers].

5.2.3. Запрет на использование ссылочных типов

Программист при разработке шаблона должен заранее решить вопрос о допустимости аргументов ссылочного типа. Если допустимы, то, возможно, придется использовать специальною обработку для таких аргументов. Если недопустимы, то надо запретить их использование. Наиболее современный вариант — это использование концептов, появившихся в C++20.

template<typename T> requires (!std::is_reference_v<T>) class X { /* ... */ };

В более старых версиях можно использовать static_assert().

В стандартной библиотеке аргументы шаблона ссылочного типа для некоторых шаблонов запрещены, например, для контейнеров. В качестве примера шаблонов, где аргументы шаблона ссылочного типа допустимы, можно привести std::pair<>, std::tuple<>.

5.3. Стандартный эмулятор ссылок

В этом разделе описывается шаблон класса std::reference_wrapper<>. Этот шаблон позволяет создать «нормальный» тип, у которого нет ограничений ссылочного типа, но интерфейс максимально к нему близок. Такой шаблон можно назвать эмулятором ссылки.

5.3.1. Как устроен

Пусть T аргумент шаблона, то есть тип, которым конкретизируют шаблон. Традиционная реализация — это обертка над указателем на T. Понятно, что T не может быть ссылочным типом, указатели на ссылку запрещены. А вот константным типом может. Аргумент конструктора — это lvalue типа T, T&. Понятно, почему не rvalue, в этом случае мы бы сразу получили висячую ссылку. Висячую ссылку можно получить и для lvalue-аргумента, это зависит от времени жизни экземпляра класса по сравнению со временем жизни аргумента конструктора. Если не использовать автоматический вывод аргумента шаблона (C++17), то в качестве аргумента конструктора также можно использовать lvalue /rvalue любого типа, имеющего неявное преобразование к T&. Конструктора по умолчанию и, соответственно, возможности создать нулевую ссылку нет. Класс не поддерживает управление жизненным циклом объекта, на который указывает указатель, — деструктор ничего не делает. Семантика копирования — по умолчанию, просто копируется указатель. Перемещающее копирование не поддерживается, так как нет нулевых ссылок. Семантика присваивания — по умолчанию, происходит присваивание указателей. Обратим внимание, что эта семантика отличается от семантики присваивания для ссылок — присваивание ссылок реализовано, как присваивание объектов, на которые они ссылаются. Класс имеет неявное преобразование к T&. Это позволяет использовать экземпляры класса для инициализации ссылок на T и в качестве аргумента в функциях, в которых принимаются ссылки на T.

void Foo(int& rx); int x = 6; std::reference_wrapper<int> rwx = x; int &rx = rwx; // OK Foo(rwx);      // OK

А вот изменить значение, на которое ссылается экземпляр, с помощью присваивания или вызвать функцию-член класса T нельзя. Для решения этой задачи надо сначала вызвать функцию-член get(), которая возвращает T&.

int x = 6; std::reference_wrapper<int> rwx = x; rwx = 32;       // ошибка rwx.get() = 32; // OK

Тип T может быть типом функции. На этот случай в классе перегружен оператор ().

void Foo(int x); std::reference_wrapper<void(int)> rwf = Foo; rwf(32);

Для создания экземпляра класса можно использовать шаблон функции std::ref<>(), который может выводить аргумент шаблона класса.

int x = 6; auto rwx = std::ref(x); // то же, что и  // std::reference_wrapper<int> rwx = x;

Также можно использовать шаблон функции std::сref<>(). В этом случае аргумент шаблона класса выводится как константный тип.

const int x = 6; auto сrwx = std::сref(x); // то же, что и  // std::reference_wrapper<const int> crwx = x;

5.3.2. Использование

Конкретизации шаблона std::reference_wrapper<> являются «нормальными» типами, их можно использовать для создания массивов, в качестве аргументов для стандартных контейнеров и других шаблонов. Если у нас есть шаблон функции

template<typename T> void Foo(T param);

то при использовании аргумента, являющегося конкретизацией шаблона std::reference_wrapper<>, мы по существу заменяем передачу параметра по значению на передачу параметра по ссылке. Гарантии того, что с таким аргументом шаблон будет успешно конкретизирован, нет, но определенные доработки шаблона могут решить эту проблему, см. примеры ниже.

В стандартной библиотеке иногда применяется следующий прием: если аргумент шаблона функции имеет тип std::reference_wrapper<T>, то он преобразуется в T&, в противном случае остается неизменным. Приведем примеры.

int x = 1, y = 2; auto rp1 = std::make_pair(std::ref(x), std::ref(y));

Тип rp1 будет выведен, как std::pair<int&, int&>.

Того же эффекта можно достичь при использовании конструктора и непосредственно указав аргументы шаблона как ссылочные.

int x = 1, y = 2; auto rp2 = std::pair<int&, int&>(x, y);

Тип rp2 также будет выведен, как std::pair<int&, int&>. Получилось даже еще и короче, но мы вынуждены явно задавать аргументы шаблона класса, автоматический вывод здесь работать не будет.

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

// header <utility> // namespace std // // ALIAS TEMPLATE _Unrefwrap_t template <class _Ty> struct _Unrefwrap_helper { // leave unchanged if not a reference_wrapper     using type = _Ty; }; template <class _Ty> struct _Unrefwrap_helper<reference_wrapper<_Ty>> { // make a reference from a reference_wrapper     using type = _Ty&; }; // decay, then unwrap a reference_wrapper template <class _Ty> using _Unrefwrap_t = typename _Unrefwrap_helper<decay_t<_Ty>>::type;

По такому же принципу реализован шаблон функции std::make_tuple().

Шаблон std::reference_wrapper<> может оказаться полезным при разработке других шаблонов, но возможность его использования должна быть предусмотрена заранее.

6. Список статей серии «C++, копаем вглубь»

1. Перегрузка в C++. Часть I. Перегрузка функций и шаблонов.
2. Перегрузка в C++. Часть II. Перегрузка операторов.
3. Перегрузка в C++. Часть III. Перегрузка операторов new/delete.
4. Массивы в C++.
Семантика перемещения подробно обсуждается в статье «Семантика копирования и управление ресурсами в C++».

7. Итоги

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

2. Категория значения выражения — важнейшее понятие, необходимое для описания правил использования ссылок. Имеются две основные категории значения — lvalue и rvalue.

3. Rvalue-ссылки — это разновидность ссылок, которая появилась в C++11. С помощью них реализуется семантика перемещения — одно из самых значительных нововведений C++11.

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

5. Ссылочные типы широко применяются в качестве типов параметров и возвращаемого значения функций. Это позволяет увеличить эффективность, функциональность и читаемость кода, поддержать полиморфизм и семантику перемещения. Ключевую роль играют правила перегрузки функций, которые имеют параметры ссылочного типа.

6. В C++ нет универсального механизма предотвращения появления висячих ссылок. Эта задача ложится на плечи программиста. Потенциальный источник висячих ссылок — функции, возвращающие ссылку, и члены класса ссылочного типа, с ними надо быть особенно аккуратным.

7. Ссылочные типы имеют ряд ограничений, их использование в качестве аргументов шаблона в общем случае не запрещено, но в отдельных случаях может вызвать проблемы. По этой причине некоторые шаблоны вынуждены ввести запрет на использование ссылочных типов в качестве аргументов шаблона. Для решения проблем, связанных с ограничениями ссылочных типов, может оказаться полезным шаблон класса std::reference_wrapper<>.

Список литературы

[VJG]
Вандевурд, Дэвид, Джосаттис, Николаи М., Грегор, Дуглас. Шаблоны C++. Справочник разработчика, 2-е изд.: Пер. с англ. — СПб.: ООО «Диалектика», 2020.

[Dewhurst]
Дьюхэрст, Стефан К. Скользкие места C++. Как избежать проблем при проектировании и компиляции ваших программ.: Пер. с англ. — М.: ДМК Пресс, 2012.

[Meyers]
Мейерс, Скотт. Эффективный и современный C++: 42 рекомендаций по использованию C++11 и C ++14.: Пер. с англ. — М.: ООО «И.Д. Вильямс», 2016.


ссылка на оригинал статьи https://habr.com/ru/post/646005/

Потратить миллион, чтобы нанять двух джунов: профит и около него

Привет, меня зовут Валерий Антонов, я руковожу направлением Java в Уральском банке реконструкции и развития (УБРиР). 

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

Предпосылки

Вторая половина этого года для УБРиР ознаменовалась значительным ростом потребности в разработчиках разного уровня. При этом зарплатные ожидания кандидатов взлетели на такие высоты, откуда землю-матушку уже и не разглядеть. Как не разглядеть и собственных проблем в элементарных знаниях.

Рынок диктует, какие технологии востребованы, за что больше платят. Разработчики учат то, что чаще упоминается в вакансиях, оставляя нюансы на потом. Эдакий технический долг, который тянется до лучших времен. Не будем же их осуждать, в конце концов, мы не видели проектов без техдолга. Перед тем, как запустить школу, мы активно искали разработчиков на рынке и выделили два условных перекоса у разработчиков:

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

  • кандидаты слабо разбираются во фреймворках, что, впрочем, не мешает им писать что-то однотипное. Человеку дали функцию, а он по аналогии как-то сделал. Может он однотипные домики лепить, но у нас домиков нет, у нас здоровенный котлован. И вот с ним у кандидатов уже начинаются проблемы.

Итог: и те, и другие не подходили для наших масштабных работ. Большие мускулы, но тоненькие ножки. Таких специалистов можно нанять и переучить, но они просят большие деньги. И процесс обучения не стоит того. Так появилась идея самим подготовить себе кадры.  

Когда учил только Spring, но пропускал лекции по Java Core

О сроках

Обучение в первой школе длилось 4 месяца, во второй — в 4 раза меньше. Почему мы решили сократить сроки обучения? Первая причина упирается в деньги: бюджет банка не резиновый. Лектор, проверка домашних работ – все это затраты, которые увеличиваются с продолжительностью курса. Вторая – время текущих сотрудников. Мы не хотели затягивать обучение, чтобы долго не отвлекать сотрудников с домашками и дополнительной нагрузкой. 

Правда, потом мы очень пожалели, что сильно сократили сроки обучения во второй школе. Ребята не успевали сделать домашние задания, и в итоге — подготовиться к дипломной работе. Хотя в результате ребятам немного “свезло”: из-за больничного и отпуска лектора у студентов образовался зазор, во время которого они успели нагнать программу и закрыть свои “хвосты”.  

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

Воронка

Первое звено: 55 сотрудников + 7 внешних кандидатов

Первый отклик от сотрудников на открытие школы Java — 55 человек. Разработчики PHP/Oracle/АВAP, аналитики, ребята из техподдержки и сотрудники самого банка заполняли анкету, чтобы разобраться, зачем им школа и как они будут применять полученные знания в работе. 

Кто-то хотел пересесть на Java, кому-то пришлось самому разбираться в ней для проекта и хотелось поднять свой уровень. Интересно, что среди аналитиков было большое количество желающих поучиться, при этом у них не стояла цель стать разработчиками на Java, они хотели расти в профессиональном плане — более грамотно ставить задачу. Если аналитик знает, как работает код, то он сможет точнее поставить задачу, заранее понимать, сработает ли всё так, как хочет заказчик, или нет, возможно-невозможно. 

Аналитиков для обучения мы не взяли. Во-первых, им не был нужен тот уровень, который давали в школе. Во-вторых, все лекции и материалы презентаций обучения мы открыли для сотрудников банка. Лекций без практических заданий хватает, чтобы закрыть потребности в знаниях аналитиков. 

Открывайте доступ на внешнее обучение своим сотрудникам. Это поможет сохранить те кадры, которые сейчас работают, страдают, но молчат.

Второе звено: 8 сотрудников + 7 внешних кандидатов.

За два потока школы прошли 21 человек. В первый поток мы включали только сотрудников, так как нам требовались погруженные в банковские процессы люди. Мы не были готовы обучать новичков, для них элементарно не нашлось бы менторов. Сотрудников выбирали по следующему принципу: у кого сейчас в проекте есть Java или кто мог бы мигрировать со своего проекта на Java-проект. К слову, часть сотрудников собиралась сменить компанию именно из-за стека, они хотели перейти на другую технологию, но у нас пока не было такой возможности. О планах некоторых сотрудников покинуть УБРиР мы узнали позже, и большой радостью стало, что школу мы уже запустили и взяли конкретных сильных людей, избежав затрат на подбор, адаптацию и закрытие кадровых дыр в проектах.

Второй поток мы разделили на две части: текущие сотрудники и кандидаты, когда-то не прошедшие собеседование в компанию. У нас сохранился список начинающих разработчиков, которые не смогли пройти техническую часть собеседования, но были потенциально перспективными и приятными людьми. Рекомендую: договаривайтесь о возможном сотрудничестве с потенциально подходящими приятными кандидатами заранее, когда у вас только появляются мысли об обучении. Это сократит время на подбор студентов. 

Третье звено: джава-разработчики 5 сотрудников + 2 внешних кандидата

Стратегия брать в студенты сотрудников не дает 100% прохождения воронки. Три человека в первом потоке не стали менять проект и пересаживаться на Java, хотя прошли обучение до конца и защитили диплом. Мы связали это с тем, что человек не хотел уходить из конкретной команды и готов вернуться к коду на Java, когда мы будем добавлять технологии Java в его команде.

Из внешних кандидатов мы взяли двух лучших учащихся и определили в команды. Я не был удивлен этими результатами и даже не ожидал принять больше. Да и больше джунов мы бы просто не устроили в команды. 

Не стоит ждать, что после обучения к вам придет большой поток сотрудников, мы, например, закладывали 20%. Если у вас был такой опыт — поделитесь, каким оказался процент трудоустройства с курсов или внешнего обучения. К слову, у онлайн-школ примерно 10% учеников находят работу.

Как строилось обучение

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

Занятия проходили два раза в неделю по 3 часа. плюс домашние задания. Задания проверялись так: выполненное задание заливается в git-репозиторий, преподаватель проверяет и дает комментарий. Мы отказались от платформы для общения ученик-учитель, в пользу git-репозиториев и общих чатов. Так ученики получали ответы в чатах быстрее и могли помогать друг другу, а отправка заданий создавала привычку использования гита.они показались нам слишком песочницами. 

Основные сложности были с программой обучения, потому что аудитория курса оказалась слишком разношерстной. Семь человек на начинающем уровне Java, двое пишут на Java в компании из производственной необходимости, у них образовался разрыв между текущими задачами и фундаментальными знаниями, остальные сильные мидлы, которым не нужны прописные истины, но нужны тонкости языка. 

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

Столкнулись мы и с другой проблемой – временные и финансовые ограничения отбросили часть программы. Ниже приведу программу месячного курса второго потока.

(скрываемый-раскрываемый раздел)

  1. Основы объектно-ориентированного программирования 

  2. Реализация ООП в Java

  3. SOLID принципы

  4. Коллекции

  5. Потоки

  6. Исключения

  7. Тестирование

В первом потоке дополнительно изучались Spring Boot и Hibernate

Когда строите школу, отвечайте на вопросы:

1. какой уровень знаний и опыта у аудитории; 

2. что для них будет полезной информацией, а на что вы просто потратите ресурсы;

3. есть ли внутренние возможности самим давать эти знания или лучше привлечь партнера.

И что же в итоге? 

Что в результате мы получили:

  • 5 “официальных” джавистов  и 10 амбассадоров, способных включаться в разработку на Java в случае необходимости; 

  • 2 новых сотрудника-джуна;

  • база знаний для аналитиков и всех желающих поднять свой уровень образования; 

  • проект, мотивирующий сотрудников делиться знаниями

О последнем пункте расскажем подробнее. Школа получилась вовлекающей: несколько МР-ов выступили перед студентами и рассказали о своих командах. Кроме этого специалисты банка прочитали студентам лекции про использование системы контроля версий и использование системы багтрекинга, а аджайл-коучи организовали тренинг про жизнь в аджайл. Почему это было полезно? Во-первых, внешние ребята лучше узнали структуру команд. Во-вторых, такой формат обучения стал первыми шагами в построении комьюнити банка. Но это совсем другая история)

Отобрать новых сотрудников из числа студентов было просто — по итогам дипломных работ. Двое студентов защитили дипломные работы на высоком уровне без дополнительного времени на переделывание, и после знакомства команд с кандидатами мы забрали этих ребят в штат. На этом отбор закончился. 

Собирайте лекции и дополнительные материалы в отдельную папку с удобной навигацией. Когда вас начнут расспрашивать “а где посмотреть можно?”, вы скажете себе спасибо)

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

Закладывайте время на дальнейшую поддержку своих студентов, обязательно выделяйте менторов и организуйте консультации, встречи для обсуждения проблем. “Варите” своих студентов в одном котле и помогайте им не выгореть после новых навыков.

Ну и о профите

Планируя обучение, мы рассматривали за основу принцип Парето, принцип 80/20. Превышение целевого показателя в 20% можно рассматривать как несомненный успех проведенных мероприятий. Первый поток: на входе 6 сотрудников, из них 3 стали разработчиками на Java

Второй поток: на входе 7 сотрудников, 2 разрабатывают на Java; на входе 8 внешних кандидатов, из них 2 приняты разработчиками в штат банка. Хочется добавить, что у нас оказалось более продуктивным внутреннее обучение, а не набор извне. Мы решили не брать на себя обучение новичков, но интегрировать с помощью стажировок тех, кто уже где-то учился и готов к развитию. Оставим внутренние школы, чтобы поднимать уровень знаний собственных сотрудников, этот вариант оказался самым благоприятным.


ссылка на оригинал статьи https://habr.com/ru/company/ubrr/blog/599973/

Невзаимозаменяемые токены (NFT) — явление года. Что это такое и каков их юридический статус в России?

Мировой рынок охватила новая мания под названием NFT, или невзаимозаменяемые токены (non-fungible tokens). Издательство Harper Collins назвало это словом года. Речь идет о технологии, с помощью которой любой человек может закрепить за собой право владения любым цифровым объектом искусства. Музыканты уже продают подобным образом свои альбомы, пользователи Твиттера — написанные сообщения, а художники — созданные ими цифровые картины. 

Объем рынка NFT за минувший год совершил невероятный скачок с $100 млн до $22 млрд., cвидетельствует аналитический сервис DappRadar. А в отчете Chainanalysis называется и того большая цифра — $26,9 млрд. По мнению аналитиков, решающим фактором этого стало продвижение NFT с помощью таких всемирно известных брендов, как Coca-Cola, Nike, Adidas и Gucci.

«Столько хайпа вокруг NFT возникло в том числе из-за нестыковки традиционного арт-мира и непонятно откуда возникшего рынка криптоискусства, в котором вдруг зазвучали астрономические суммы. Если до появления NFT ценник на произведения искусства устанавливали художественные галереи, то с появлением блокчейна любой автор получил возможность не только сам себя оценить, но и сам себя продать. <…> NFT пришли навсегда. Другое дело, что когда-нибудь эта технология будет для человечества как черно-белый телевизор, ей на смену придут более совершенные и безупречные аналоги, которые расширят и углубят открытые блокчейном возможности», — считает цифровая художница Meta Rite.

Спрос на невзаимозаменяемые токены стал по-настоящему ажиотажным после того, как в марте художник Майк Винкельман, известный под псевдонимом Beeple, продал на аукционе Christie’s свою картину Everydays: The First 5000 Days в виде NFT-токена за рекордные $69,3 млн, и в результате стал одним из трех самых дорогих ныне живущих художников. Выше оценивались только картины Джеффа Кунса (91,1 миллиона долларов) и Дэвида Хокни ($90,3 млн.).

Cреди самых заметных игроков NFT-рынка, помимо Винкельмана, называют также покупателя той самой работы Beeple «Первые пять тысяч дней» и создателя собственного инвестиционного NFT-проекта Metapurse — Виньеша Сундаресана aka MetaKovan, а также художников Тома Сакса, FEWOCiOUS (Виктора Ланглуа), Кевин и Дженнифер Маккой и Osinachi (Принц Джейсон Осиначи Игве).

В феврале 2021 года супруга Илона Маска певица Граймс на площадке Nifty Gateway выручила $5,8 миллиона долларов за 20 минут торгов. На продажу она выставила коллекцию изображений и видео, которую создала вместе со своим братом. А в марте того же года основатель Twitter Джек Дорси выставил на площадке Valuables NFT первый в истории твит и продал его за 2,9 миллиона «зеленых».

Не остался в стороне от NFT-технологий и самый известный художник современности. Блокчейн-компания Injective Protocol провела публичную акцию сожжения картины Бэнкси под названием Morons (White) и «конвертировала» ее в NFT-токен. Организаторы называют это «неким визуальным выражением артистического действа в реальном времени» и первым примером, когда подлинное произведение искусства было переведено во невзаимозаменяемый токен. Через несколько дней цифровую копию картины в Injective Protocol продали за $380 000. 

По данным издания The Bell, в ноябре 2021 года на планете было примерно 360 тысяч владельцев NFT, которым принадлежало 2,7 млн невзаимозаменяемых токенов. При этом наиболее ценные из них, а это примерно четыре пятых от всего объема рынка NFT, были сосредоточены в руках 32 тысяч человек (9% от общего количества владельцев).

Невероятные приключения NFT в России

Что касается рынка non-fungible tokens в РФ, то, бесспорно, истинной сенсацией 2021 года стал выпуск летом Государственным Эрмитажем собственной коллекции NFT, куда вошли цифровые копии великих шедевров — среди которых были «Мадонна Литта» Леонардо да Винчи, «Юдифь» Джорджоне, «Куст сирени» Винсента Ван Гога, «Композиция VI» Василия Кандинского, «Уголок сада в Монжероне» Клода Моне. Судя по всему, Эрмитаж решил взять пример с итальянской галереи Уффици, которая минувшей весной 2021-го выпустила криптокопию одной из работ Микеланджело Буонарроти — «Мадонна Дони». Ее продали за €140 тыс.

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

Это было только началом экспансии главного музея страны в мир NFT. 10 ноября в Эрмитаже открылась виртуальная экспозиция «Незримый эфир», где были представлены картины и медиафайлы, созданные с помощью блокчейна. 

Самый известный в России #прямосейчас уличный художник — Покрас Лампас — выставил на криптоаукцион Foundation.app проекцию своего полотна на крупнейшую гидроэлектростанцию Северного Кавказа — Чиркейскую ГЭС, и продал его за $29 тыс. «Я вижу NFT как технологию верификации уже существующего арта, это универсальный сертификат аутентичности для полотна», — заявил художник в разговоре с РИА «Новости».

А арт-дуэт ППСС (художники Павел Пепперштейн и Соня Стереостырски) создал невзаимозаменяемый токен своей работы «Новый мир» из лоскутов ткани. Цена на него на платформе Rarible остановилась на отметке около $15 тыс.

В этой статье мы попробуем разобраться с тем, что такое технология NFT, а также каков ее юридический статус в действующем законодательстве РФ (и имеется ли он вообще).

Что такое NFT-токен и зачем он нужен

Изначально технология NFT была создана в 2017 году на основе смарт-контрактов, привязанных к криптовалюте Ethereum. 

Смарт-контракты дают возможность детально прописать в блокчейне условия соглашения и механизм его строгого выполнения. По сути, это цифровой аналог бумажных договоров. Закрепить за собой владение каким-то активом по смарт-контракту можно с помощью токена. Это тоже цифровой аналог — в данном случае аналог ценной бумаги, известный как security token.

Большинство токенов являются взаимозаменяемыми (fungible) — то есть они равны друг другу и могут использоваться в качестве валюты (например, такими токенами обладают несколько инвесторов одной и той же компании). Есть и наполовину взаимозаменяемые токены (semi-fungible), обладающие каким-то одним уникальным свойством, позволяющим отличать их друг от друга. Например, эти токены могут закреплять за их владельцами право на получение билета на сеанс в кино — в таком случае цена на них будет одинаковая, а закрепленные за владельцами токенов места в кинотеатрах — разные. 

Невзаимозаменяемые же токены (non-fungible tokens или NFT) полностью уникальны — и дают возможность владения цифровым объектом, который существует в единственном экземпляре: музыкальными произведениями, цифровыми художественными объектами, игровыми предметами (оружие, персонажи) и любыми другими уникальными виртуальными вещами. 

Учитывать посредством NFT можно и объекты реального мира, по аналогии с тем, как в реестрах учитывают недвижимость или автомобили. Но с объектами вещественного мира основные преимущества децентрализованного цифрового учета исчезают – характеристики таких объектов сложно однозначно определить, их купля-продажа невозможна исключительно путем передачи информации, нужно передавать и саму вещь. Поэтому NFT используют сейчас главным образом как глобальный реестр учета прав на цифровые объекты: фотографии, музыку, платные предметы в компьютерных играх и т.п.

При проведении сделки с NFT пользователь покупает только уникальный цифровой сертификат на произведение искусства, записанный на блокчейне, а само произведение при этом никуда не перемещается. Все хранится на вечном хранилище (так называемом IPFS — Inter-Planetary File System, это одноранговая распределенная файловая система, которая соединяет все вычислительные устройства воедино). Такой блокчейн-сертификат (по структуре своей всего лишь строчки кода, записанные на блокчейне) подтверждает, что именно вы владеете оригинальной копией этого объекта — несмотря на то, что его может посмотреть или скачать абсолютно любой человек. Точно так же любая картина может принадлежать музею или частному лицу, но вы можете увидеть ее на выставке или в каталоге. 

«Автор, как и любой пользователь интернета, может хранить картинку где угодно: на телефоне, флешке, компьютере, планшете, но это ничего не значит. Цифровую работу можно по-прежнему скачивать, копировать и пересылать, но обладателем подлинника будет только тот, кому принадлежит токен. Раньше отсутствие у цифрового искусства оригинала было большой проблемой, его было невозможно продать, не придав ему физическую форму. NFT решило этот вопрос, и в этом революционность этой технологии», — считает художница Meta Rite.

Reuters подчеркивает, что, как и на многих новых нишевых инвестиционных площадках, на рынке NFT есть риск понести убытки в случае, если ажиотаж вокруг технологии угаснет, а также существует опасность стать жертвой мошенников, поскольку многие участники торгов скрывают имена за псевдонимами. 

«Поскольку выпустить NFT-токен на произведение может любое лицо, у которого имеется к нему доступ, возможна ситуация, при которой беститульный токенизатор (человек, не имеющий права на произведение) недобросовестно использует и коммерциализирует чужие произведения. Механизмы регулирования, позволяющие ограничить такие случаи, должны быть сосредоточены на арт-платформах, которые допускают генерацию токенизированных NFT-произведений. В числе таких мер — требования по обязательной идентификации и верификации пользователей, внедрение дополнительных инструментов проверки подлинности токенизированного контента и доказательств прав на него», — рассказала в интервью «Ъ» эксперт Екатерина Портман, директор Deloitte Legal на территории СНГ.

Тем не менее, как объясняет сооснователь нью-йоркской платформы для продажи и покупки цифрового искусства Snark.art Андрей Алехин, «NFT дает возможность даже в цифровом мире, где нет границ и физических копий, все равно заявить о том, что вы так или иначе «владеете» конкретной работой или предметом коллекционирования. Поэтому NFT и получил широкое применение в арт-индустрии, особенно сейчас, во время глобальной пандемии, когда практически все выставки и художественные ярмарки либо были закрыты, либо перешли в онлайн».

«NFT-волна вынесла цифровой арт на передовой край общественного внимания. Только за эти полгода мои работы приняли участие в шести выставках, двух форумах и одной международной конференции <…> Не нужно печатать мерч, не нужно накатывать арт на пенокартон, не нужно идти с ним на Почту России. Одновременно с этим, одним щелчком мыши ты попадаешь на международный рынок цифрового искусства. Да, на нем все не просто, но он есть и это не может не радовать», — говорит Meta Rite.

А по словам Александра Сальникова, сооснователя Rarible – а это один из крупнейших в мире NFT-маркетплейсов, основанный  выходцами из России, — до NFT просто не существовало эффективной технической возможности владения цифровыми объектами, что «значительно ограничивало возможности монетизации работы digital-художников». 

Юридический статус NFT-токенов и сделок с ними на мировом крипторынке

Рассмотрим для примера пользовательское соглашение платформы OpenSea — одной из самых популярных площадок для приобретения «нифти». В этом соглашении записано, что размещая или отправляя те или иные цифровые активы с помощью платформы, вы тем самым безвозмездно предоставляете OpenSea неисключительную лицензию, действующую на территории всего мира с правом заключать сублицензионные договоры. 

Подобная лицензия выдается для следующих обозначенных целей: использование, копирование, воспроизведение, обработка, адаптация, изменение, публикация, передача, отображение и распространение любыми способами. Отдельно подчеркивается, что предоставление такой лицензии не ограничивает прав собственника того или иного актива, а также отмечается, что OpenSea не имеет каких бы то ни было прав на продажу ваших активов: «We’re [OpenSea] just saying we might show it off a bit».

А в правилах уже упоминавшейся нами цифровой платформы Rarible специально указано, что ее пользователи не вправе создавать NFT-токены, которые нарушают права третьих лиц. Вопрос же перехода исключительных прав на сами объекты интеллектуальной собственности пока что создателями Rarible никак не урегулирован.

«Поскольку вопросы передачи права чаще всего регулируются соглашениями между арт-платформой либо правообладателем и приобретателем, эти документы могут служить доказательствами принадлежности прав на токенизированный объект», — отмечает Екатерина Портман. Таким образом, если между создателем и покупателем NFT не заключено отдельного юридического соглашения, то вместе с покупкой невзаимозаменяемого токена покупатель не получает никаких исключительных прав на произведение как объект интеллектуальной собственности. 

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

Юридический статус NFT-токенов и сделок с ними в России, с учетом Закона о цифровых финансовых активах

Напомним, что цифровая валюта, согласно п. 3 ст. 1 Федерального закона от 31.07.2020 N 259-ФЗ «О цифровых финансовых активах, цифровой валюте и о внесении изменений в отдельные законодательные акты Российской Федерации» (далее — Закон о ЦФА), — это, если максимально упростить, совокупность электронных данных, которые можно использовать в качестве средства платежа, не являющегося рублем или инвалютой. А цифровые финансовые активы — это цифровые права, включая денежные требования, права на ценные бумаги, на участие в капитале организаций, существующие в виде записей в информационной системе. 

При этом выпуск ЦФА возможен только в порядке, установленном этим же Законом. Банк России ведет реестр операторов, выпускающих ЦФА. Этими операторами могут быть только юридические лица, личным законом которых является российское право.

Таким образом, юридический статус NFT-токенов сейчас российским законодательством никак не определен, поскольку они не подпадают ни под определение цифровых финансовых активов (так как выпуск NFT-токенов осуществляется не исключительно российскими юрлицами),  ни под термин «цифровая валюта» (так как невзаимозаменяемый токен не есть средство платежа).

Какое-то отдаленное подобие NFT-токенов могут создавать в РФ в рамках Федерального закона от 02.08.2019 N 259-ФЗ «О привлечении инвестиций с использованием инвестиционных платформ и о внесении изменений в отдельные законодательные акты Российской Федерации». Данный закон регламентирует работу в России инвестиционных (краудфандинговых) площадок, когда на сайте организуют сбор средств в обмен на право требовать в будущем передачу какой-либо вещи или оказание услуги, выполнение работы. Такие права в статье 8 закона именуются «утилитарные цифровые права» (по аналогии с западным названием «utility token»). Утилитарными цифровыми правами не может быть право требования имущества, сделки с которым подлежат государственной регистрации или нотариальному удостоверению. 

С выходом Закона о ЦФА возможность инвестирования на данных платформах путем приобретения утилитарных цифровых прав дополнили возможностью приобретения цифровых финансовых активов. Однако создавать подобные площадки и операторов ЦФА нужно исключительно в рамках российского законодательства. Фактически, утилитарные цифровые права не сильно подходят под определение NFT-токенов в том контексте, в котором мы говорили о них выше. 

Можно сделать вывод, что сейчас учет авторских прав посредством NFT, для нашего законодательства не существует, проходя на 100% в «серой зоне». Для российского законодателя существуют только непосредственно сами авторские права, которые передаются не посредством какого-то заморского и малопонятного блокчейна, а сугубо путем договорных отношений в рамках Гражданского кодекса РФ. Более того, статья 14 Закона о ЦФА устанавливает прямой запрет на расчёты криптовалютами за товары, и пока не очень понятно, могут ли цифровые активы в формате NFT рассматриваться как «товар». 

Операции с NFT по российскому законодательству сейчас следует рассматривать только как сделки без оформления письменного договора (п.2 ст. 159 ГК РФ). Если дело дойдет до оспаривания прав в суде, вы сможете оперировать в лучшем случае публичным соглашением с интернет-площадкой, где была проведена сделка с NFT, и распечатками из блокчейна (ч.1 ст. 162 ГК РФ). 

Не исключено использование NFT для защиты прав на интеллектуальную собственность, что-то вроде регистрации в Роспатенте, но только в блокчейне. Это поможет решить одну из главных проблем для всех децентрализованных автономных организаций. Дело в том, что на текущий момент ни одна из DAO не может быть полностью децентрализована — ей в любом случае необходим как минимум одно юридическое лицо, уполномоченное на регистрацию прав на объекты интеллектуальной собственности (например, таким образом часто регистрируют товарные знаки) от имени DAO в государственных структурах. Более того, такое использование технологии позволит быстро и дёшево регистрировать авторские и иные права на различные произведения, фактически присваивая им отдельные токены. Эффективно нечто подобное уже реализовано на международном уровне в системе WIPO PROOF. Попытки создать системы подобного учёта были и в России (проект IP Chain), однако серьёзным успехом они до сих пор не увенчались.  


ссылка на оригинал статьи https://habr.com/ru/company/digitalrightscenter/blog/646009/