Зачем нужен в c

от admin

Интерфейсы в C#: зачем они нужны?

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

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

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

Для иллюстрации мы используем типовой пример «Сотрудник в компании»:

Рассмотрим следующий сценарий: в компании есть 3 типа сотрудников: служащий (Executive), менеджер (Manager) и топ-менеджер (C-suite).

Служащий имеет имя (Name), должность (Designation) и ключевой показатель эффективности (KPI). Вот так может выглядеть класс Executive :

У менеджера все то же самое, что и у служащего, но у него еще есть дополнительные права оценивать сотрудников уровня “служащий”. Вот так может выглядеть класс Manager :

У топ-менеджеров есть имя и должность, но нет KPI. Мы предполагаем, что их KPI связаны с доходностью акций компании или другими показателями. Топ-менеджеры также имеют право оценивать только сотрудников уровня “менеджер”, и, кроме того, они имеют право уволить любого неэффективного сотрудника. Вот так может выглядеть класс CSuite :

Обратите внимание, что:

Хоть классы Manager и CSuite имеют метод EvaluateSubordinate , они принимают разные типы аргументов.

CSuite имеет две функции TerminateExecutive , которые принимают различные типы аргументов.

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

Отобразить их имя и должность.

Соответствующий руководитель оценит их KPI.

Топ-менеджер уволит тех, у кого KPI меньше 70.

И получить что-то вроде этого:

Для начала я инициализирую всех своих сотрудников:

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

Польза от интерфейса № 1: Может выступать в роли обобщенного класса

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

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

Во-вторых, поскольку KPI должны оцениваться только для сотрудников классов Executive и Manager, я создал интерфейс IEvaluatedEmployee , который имеет только одно свойство: KPI. Обратите внимание, что мой интерфейс IEvaluatedEmployee также реализует интерфейс IEmployee . Это означает, что любой класс, реализующий этот интерфейс, также будет иметь свойства IEmployee (а именно имя и должность).

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

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

Я закончил с интерфейсами, так что давайте теперь посмотрим, как наши классы будут их реализовывать.

Для класса Executive :

Для класса Manager :

Обратите внимание, что класс Manager реализует и IManagementLevelEmployee , и IEvaluatedEmployee , т.е. это указывает на то, что сотрудники, принадлежащие к этому классу, имеют право оценивать других сотрудников, но в то же время также могут оцениваться кем-то другим.

Наконец, наш класс C-Suite :

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

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

Преимущества использования интерфейсов не ограничивается на возможности группировки взаимосвязанных классов. Они также дают нам гибкость при написании функций. Давайте вернемся к функции для увольнения сотрудников топ-менеджером. Нам достаточно написать только одну функцию TerminateEmployee , которая принимает аргументы с типом IEvaluatedEmployee (который реализуют Executive и Manager), вместо двух функций для удаления Executive и Manager соответственно.

Польза от интерфейса № 2: Обязывающий “контакт” и расширение возможностей класса

Многие люди рассматривают интерфейсы как “контракты” в мире классов. Рассмотрим пример из реальной жизни: владелец обувной фабрики подписывает контракт с инвестором о том, что фабрика должна будет произведет 100 пар обуви в течении одного месяца. Фабрика ДОЛЖНА произвести 100 пар обуви для инвестора, и невыполнение этого обязательства повлечет за собой штраф.

Для примера, класс CSuite реализует два интерфейса: IManagementLevelEmployee и ICSuite_Privilege . Это “контракт”, который обязывает этот класс иметь все функции из этих двух интерфейсов (оценки и увольнения сотрудника). Если мы не позаботимся о их создании, то компилятор выдаст ошибку (аналог штрафа в реальном мире).

Скажем, в будущем будет введен новый класс под названием “Board” (правление/совет директоров). Мы сможем назначить аналогичные привилегии классу Board, чтобы гарантировать, что он будет иметь такие же полномочия, как и CSuite. Благодаря этому мы можем гарантировать, что все инстансы Board будут иметь функцию увольнения сотрудника. Эта фича дает программисту возможность быстро оценить назначение и ответственность каждого класса, просто посмотрев на интерфейсы, реализуемые ими.

Польза от интерфейса № 3: Множественное наследование для разделения ответственности

Вы могли заметить, что интерфейс IManagementLevelEmployee имеет функцию EvaluateSubordinate , и параметр, который он принимает, — IEvaluatedEmployee , а не IEmployee .

Функция EvaluateSubordinate будет так же прекрасно работать, если она будет принимать IEmployee , поскольку классы Executive и Manager также реализуют IEmployee . Так почему вместо этого я использую IEvaluatedEmployee ?

Потому, что в нашем случае:

CSuite также реализует IEmployee ,

но сам CSuite никем не оценивается,

а по какому показателю будет оцениваться сотрудник? В нашем случае это KPI.

Поэтому, чтобы удовлетворить всем требованиям, я создал интерфейс под названием IEvaluatedEmployee , у которого есть свойство KPI, и в то же время он реализует IEmployee . Таким образом, это означает, что кто бы ни реализовывал этот интерфейс (Executive и Manager), он будет иметь не только KPI, но и все свойства IEmployee (имя и должность).

Но класс CSuite не реализует интерфейс IEvaluatedEmployee . Поэтому мы можем предотвратить передачу в функцию EvaluateSubordinate инстанс класса CSuite , установив такое ограничение с помощью интерфейса. Это один из способов не нарушать нашу бизнес-логику нашим кодом.

Польза от интерфейса № 4: Анализ воздействия

И, скажем, в будущем, председатель правления компании представит новую политику для сотрудников

Топ-менеджеры тоже подлежат оценке

Введет еще два параметра (a и b) для оценки

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

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

Заставлю класс CSuite реализовывать интерфейс IEvaluatedEmployee

Введу еще два параметра a и b для EvaluateSubordinate в интерфейсе IManagementLevelEmployee

Visual Studio немедленно выдаст мне предупреждение об ошибке, указывающее, что мой CSuite не реализует IEvaluatedEmployee (отсутствует свойство int KPI), а класс Manager и CSuite неправильно реализует интерфейс IManagementLevelEmployee (функция EvaluateSubordinate требует два новых параметра).

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

Польза от интерфейса № 5: Абстракция планирования

В реальной жизни, как правило, проект разрабатывается сразу несколькими разработчиками. И очень часто мы начинаем разработку еще до окончательного оформления бизнес-требований. Таким образом, с помощью интерфейсов ведущий программист или архитектор может стандартизировать функции каждого класса. Используя тот же пример, технический руководитель может не знать, какова точная бизнес-логика для оценки сотрудника, но с помощью интерфейса он/она может наглядно продемонстрировать для всех программистов, работающих с классами Manager или CSuite , что они должны содержать функцию EvaluateSubordinate , а также иметь имя и должность для каждого сотрудника и т. д., указав это в интерфейсе.

Польза от интерфейса № 6: Модульное тестирование

И последнее, но не менее важное (хотя на самом деле это может быть одной из самых важных причин для реализации интерфейса) — модульное тестирование. Эта польза очень похожа на пользу № 5, поскольку многие компании применяют методологию Test Driven Development (TDD) в процессе разработки своего программного обеспечения, поэтому на этапе планирования оглядка на модульное тестирование очень важна до фактического начала разработки.

Допустим, есть функция CheckEmployee() для класса Executive и Manager , которая получает доступ к базе данных и проверяет информацию о сотруднике, прежде чем мы сможем его уволить.

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

Мы использовали интерфейс IEvaluatedEmployee , чтобы мокнуть класс Executive , чтобы функция CheckEmployee всегда возвращала значение true .

Если у нас не реализован интерфейс и мы используем реальный класс Executive для создания моков, Visual Studio выдаст ошибку. Это связано с тем, что система не может переопределить функцию конкретного класса, чтобы она всегда возвращала либо true , либо false .

Заключение

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

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

Зачем нужны указатели?

Heт, я пoнял пpинцип paбoты, нo нe пoнял oднoгo — ЗAЧEM? Зaчeм paбoтaть c фyнкциями-члeнaми, пepeмeнными пo ccылкe, ecли я мoгy paбoтaть c ними нaпpямyю?

0xdb's user avatar

Xyanight's user avatar

«Если кто-то зажигает звезды, значит, это кому-нибудь нужно»

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

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

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

Уже говорили о динамическом выделении памяти. Если еще про это не читали, то прочитаете. Там без указателей В ПРИНЦИПЕ не обойтись.

Это лишь несколько примеров. Чем глубже будете вникать в С++, тем больше об этом узнаете.

И еще. Не думайте, что программисты до Вас были дураками.

Для возврата нескольких значений из функции. В качестве аргумента передаётся указатель на переменную, функция записывает туда значение. Такой подход очень распространён в DirectX, OpenGL, Windows API и других библиотеках в стиле C. Для этого можно использовать и ссылки, но не рекомендуется, так как синтаксис передачи и возврата неотличим.

Для хранения адреса динамически выделенной памяти. Она отличается от обычной тем, что программист сам регулирует время жизни объектов, и её больше (а размер стека всего порядка мегабайта). Если адрес будет потерян, то память нельзя будет ни использовать, ни освободить. Возникнет утечка памяти.

Читать:
Как выделить текст в питоне

C-строка представляет собой указатель на её первый символ.

Для создания различных структур данных: связанных списков, деревья и т. д.

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

Таким образом, примений указателей очень много.

А зачем нужны почтовые адреса? Можно же общаться сразу с людьми. Но иногда приходится поручать доставку письма почтальону, которому надо сначала сказать, куда конкретно он должен доставить письмо, так как один и тот же почтальон должен уметь единообразно для адресанта доставлять письма всем адресатам. Как? Лезть в его днк нельзя, нельзя его даже переучивать. Указатель – это такая величина, в которой можно хранить адрес. А ссылка – это нечто, ведущее себя как указатель, но имеющее при этом синтаксис сразу целевой величины, которую мы по этому адресу ищем. То есть поле на конверте с адресом Пети, притворяющееся самим Петей. На это поле на конверте можно наорать вместо Пети, а обидится Петя. По указателю же можно только нанять хулигана, поручив ему наорать на Петю. Совсем же по-другому выглядит, а результат тот же. В одном случае мы голос посрвали, в другом – явно послали посредника. Но по указателю можно послать: почтальона, шпиона, хулигана и киллера, а по ссылке – только наорать на адрес на конверте, спокойно ему что-то сказать или поглазеть прямо на Петин стол, но киллеры даже не поймут, чего от них хотят, а сами мы убивать не умеем.

Тарас Атавин's user avatar

Чтобы выделять большое количество памяти. Советую прочитать про стек и кучу (heap). На стеке при большом объеме данных будет происходить переполнение.

Указатели в C++: зачем нужны, когда использовать и чем отличаются от обращения к объекту напрямую

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

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

Вопрос

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

Аналогично с методами. Почему вместо этого:

мы должны писать вот это:

Я так понимаю, что это дает выигрыш в скорости, т.к. мы обращаемся напрямую к памяти. Верно? P.S. Я перешел с Java.

Ответ

Заметим, кстати, что в Java указатели не используются в явном виде, т.е. программист не может в коде обратиться к объекту через указатель на него. Однако на деле в Java все типы, кроме базовых, являются ссылочными: обращение к ним происходит по ссылке, хотя явно передать параметр по ссылке нельзя. И еще, на заметку, new в C++ и в Java или C# — абсолютно разные вещи.

Для того, чтобы дать небольшое представление, что же такое указатели в C++, приведем два аналогичных фрагмента кода:

Ближайший эквивалент на C++:

Однако вот это – совершенно другая вещь (C++):

На самом деле, совсем нет. Работа с указателями оформлена в виде кучи, в то время как работа с объектами – это стек, более простая и быстрая структура. Если вы новичок, то у нас для вас есть материал, в котором мы подробно рассказываем, что такое стек и куча.

Строго говоря, этот вопрос объединяет в себе два различных вопроса. Первый: когда стоит использовать динамическое распределение памяти? Второй: когда стоит использовать указатели? Естественно, здесь мы не обойдемся без общих слов о том, что всегда необходимо выбирать наиболее подходящий инструмент для работы. Почти всегда существует реализация лучше, чем с использованием ручного динамического распределения (dynamic allocation) и / или сырых указателей.

Динамическое распределение

В формулировке вопроса представлены два способа создания объекта. И основное различие заключается в сроке их жизни (storage duration) в памяти программы. Используя Object myObject; , вы полагаетесь на автоматическое определение срока жизни, и объект будет уничтожен сразу после выхода из его области видимости. А вот Object *myObject = new Object; сохраняет жизнь объекту до того момента, пока вы вручную не удалите его из памяти командой delete . Используйте последний вариант только тогда, когда это действительно необходимо. А потому всегда делайте выбор в пользу автоматического определения срока хранения объекта, если это возможно.

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

  • Вам необходимо, чтобы объект существовал и после выхода из области его видимости — именно этот объект, именно в этой области памяти, а не его копия. Если для вас это не принципиально (в большинстве случаев это так), положитесь на автоматическое определение срока жизни. Однако вот пример ситуации, когда вам может понадобиться обратить к объекту вне его области видимости, однако это можно сделать, не сохраняя его в явном виде: записав объект в вектор, вы можете «разорвать связь» с самим объектом — на самом деле он (а не его копия) будет доступен при вызове из вектора.
  • Вам необходимо использовать много памяти, которая может переполнить стек. Здорово, если с такой проблемой не приходится сталкиваться (а с ней сталкиваются очень редко), потому что это «вне компетенции» C++, но к сожалению, иногда приходится решать и эту задачу.
  • Вы, например, точно не знаете размер массива, который придется использовать. Как известно, в C++ массивы при определении имеют фиксированный размер. Это может вызвать проблемы, например, при считывании пользовательского ввода. Указатель же определяет только тот участок в памяти, куда будет записано начало массива, грубо говоря, не ограничивая его размер.

Если использование динамического распределения необходимо, то вам стоит инкапсулировать его с помощью умного указателя (что такое умный указатель можете прочитать в нашей статье) или другого типа, поддерживающего идиому «Получение ресурса есть инициализация» (стандартные контейнеры ее поддерживают — это идиома, в соответствии с которой ресурс: блок памяти, файл, сетевое соединение и т.п. — при получении инициализируется в конструкторе, а затем аккуратно уничтожается деструктором). Умными являются, например, указатели std::unique_ptr и std::shared_ptr .

Указатели

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

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

Спецификатор constexpr в C++: зачем он нужен и как работает

Новички могут путать спецификатор constexpr , появившийся в C++ с версии 11, с похожим на него квалификатором const .

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

constexpr можно использовать с переменными и функциями, в том числе конструкторами и операторами if . Разберемся в этом подробнее.

Переменные

Квалификатор const указывает компиляторам и другим программистам, что переменная доступна только для чтения. Любая попытка изменить ее приведет к ошибке компиляции.

В данном случае constexpr похож на const тем, что подразумевает const . Его тоже нельзя изменить. Разница в том, что const может быть вычислен как на стадии компиляции, так при выполнении программы, в зависимости от выбранного варианта инициализации.

В этом примере val вычисляется во время компиляции. При выполнении кода val всегда равно 3 . Однако в следующем примере val вычисляется во время выполнения кода, поскольку включает вызов функции.

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

Но это не применимо ко второму примеру. Мы получим ошибку компиляции из-за невозможности вычисления на этом этапе.

Чтобы устранить эту проблему в данном примере, можно преобразовать функцию в constexpr .

Код выглядит так же, но теперь выполняется во время компиляции. При выполнении компилятор модифицирует код так:

Здесь не указаны const и constexpr , потому что после запуска программы они больше не используются. И const , и constexpr применяются только в процессе компиляции.

Функции и конструкторы

constexpr можно также использовать с функциями и конструкторами. Как и в предыдущем примере, можно определить функцию или конструктор как функцию constexpr .

Функции constexpr отличаются гибкостью. Одна и та же функция может быть вычислена во время компиляции и выполнения. Все зависит от того, как они вызываются.

В этом примере есть функция constexpr под названием Sum , которая вызывается в строках 6 и 8. Строка 6 вычисляется при компиляции (как и в примере предыдущего раздела), а строка 8 — при выполнении, поскольку включает неконстантную переменную var .

Очевидно, что функция constexpr весьма полезна. Мы можем предоставить компилятору выбор: вычислять ее при компиляции или при выполнении. Для вычисления во время компиляции должен выполняться целый ряд условий (подробнее об этом по ссылке).

Операторы if

Начиная с C++17, можно определять if -операторы constexpr . Если вы не часто сталкиваетесь с общим кодом в повседневной работе, то, возможно, пользуетесь этой возможностью. Чтобы выяснить, зачем она необходима, нужно понимать концепцию SFINAE (Substitution Failure is not an Error, ошибка замены не является ошибкой) в метапрограммировании шаблона C++.

SFINAE и std::enable_if

При вызове функции компилятор должен выполнить проверку, чтобы узнать, какую из функций вызывать. Учитывайте перегрузку функций (function overloading), позволяющую давать нескольким функциям одинаковые имена. Говоря упрощенно, есть несколько шагов:

  • Name lookup (поиск по имени);
  • Template Argument Deduction (дедукция аргумента шаблона);
  • Template Argument Substitution (замена аргумента шаблона);
  • Overload Resolution (разрешение перегрузки).

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

Алгоритм SFINAE проявляется на шаге 3 (Template Argument Substitution), когда функция-кандидат проваливает тест Substitution (замены). Ошибка компиляции не возникает, а функция просто удаляется из списка кандидатов, как в следующем примере.

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

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

Однако этот шаблон функции не работает, когда ему передается объект типа Number<int> .

Строка 9 не скомпилируется, потому что Number<int> не реализует operator* .

Для решения этой проблемы нужно знать тип, переданный шаблону функции Square() , и добавить в него оператор проверки if-else , который будет вычисляться во время компиляции:

Но такое решение не работает: при вызове, например с int , эта функция пытается найти int.value , которого не существует. Чтобы увидеть это более четко, смотрим результат создания экземпляра шаблона.

Теперь понятно, почему он не работает. Часть else не удалена из функции, и мы получаем ошибку компиляции.

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

Здесь указаны два шаблона функций: для арифметических и неарифметических типов. У std::enable_if с typedef :: type , если ему передается значение true , будет спецификатор доступа public. В противном случае члены с typedef с спецификатором доступа public будут отсутствовать.

При передаче Number<int> в Square() первый шаблон функции завершает замену шаблона с ошибкой, а второй — успешно. Ошибки для первой функции не возникает, она просто удаляется из списка функций-кандидатов. Затем компилятор выбирает вторую функцию.

С помощью двух и более шаблонов функций с std::enable_if мы в некотором роде имитируем if-else во время компиляции.

Как оператор constexpr if улучшает SFINAE

Взаимодействие SFINAE и std::enable_if работает и часто используется, но не очень интуитивно понятно. Такой излишне подробный код и незнакомый синтаксис порой трудно читать.

Сделать его более читаемым, начиная с C++17, позволяют if -операторы constexpr . Можно использовать настоящий if-else во время компиляции в одной функции, а не имитировать его, используя несколько функций с std::enable_if . Ниже показана реализация с помощью if -оператора constexpr .

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

Похожие статьи