惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Tenable Blog
K
Kaspersky official blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
T
The Exploit Database - CXSecurity.com
Cisco Talos Blog
Cisco Talos Blog
P
Palo Alto Networks Blog
Latest news
Latest news
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
CXSECURITY Database RSS Feed - CXSecurity.com
P
Privacy International News Feed
The Hacker News
The Hacker News
T
Tor Project blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
C
Cisco Blogs
阮一峰的网络日志
阮一峰的网络日志
Recent Commits to openclaw:main
Recent Commits to openclaw:main
博客园_首页
N
News and Events Feed by Topic
W
WeLiveSecurity
罗磊的独立博客
GbyAI
GbyAI
T
Troy Hunt's Blog
Y
Y Combinator Blog
Recorded Future
Recorded Future
The Cloudflare Blog
TaoSecurity Blog
TaoSecurity Blog
爱范儿
爱范儿
美团技术团队
Attack and Defense Labs
Attack and Defense Labs
C
Check Point Blog
Engineering at Meta
Engineering at Meta
Cyberwarzone
Cyberwarzone
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
F
Fortinet All Blogs
The GitHub Blog
The GitHub Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Apple Machine Learning Research
Apple Machine Learning Research
Know Your Adversary
Know Your Adversary
AWS News Blog
AWS News Blog
D
DataBreaches.Net
Recent Announcements
Recent Announcements
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
M
MIT News - Artificial intelligence
Webroot Blog
Webroot Blog
Security Latest
Security Latest
T
Tailwind CSS Blog
V2EX - 技术
V2EX - 技术
aimingoo的专栏
aimingoo的专栏
S
Security @ Cisco Blogs
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Непослушный using
dalerank · 2026-05-18 · via Все публикации подряд на Хабре

Уровень сложностиПростой

Время на прочтение11 мин

Охват и читатели4

В прошлой статье я разобрал, как работает квалифицированный поиск и как using namespace участвует в нём только в качестве запасного варианта, когда собственных объявлений в указанной области нет. Компилятор сначала смотрит, что объявлено непосредственно в текущем контексте, и только при неудаче переходит к именам, подмешанным через директиву using. Казалось бы, схема прозрачная и предсказуемая: есть область поиска, есть приоритет явных объявлений, есть «правило N-объявлений» как страховка.

Но как только мы переходим от переменных и функций к более общим механизмам в коде, эта прозрачность сразу начинает ломаться, причём в самом обыденном коде, который пишет каждый разработчик с первых дней обучения. По правилам языка мы можем разместить директиву using namespace где угодно, но если в области, указанной в квалификаторе, что-то объявлено явно, квалифицированный поиск найдёт именно это объявление, и лишь если явно объявленного имени нет, компилятор начинает учитывать имена, ставшие видимыми через using namespace, и так далее по цепочке.

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


std::cout << "hello";

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

operator<<(std::cout, "hello");

Но, чтобы это работало так как мы сейчас видим функция operator<< должна быть найдена в пространстве имён std, то есть на на самом деле нам нужен вызов вида, которого в чистом виде нет

std::operator<<(std::cout, "hello");

Но если вы посмотрите на исходный синтаксис std::cout << "hello", то в чистом виде квалификатора std:: там нет, вернее он есть, но только для левого операнда cout  и формально std:: вообще никак не связан с именем оператора. Как тогда компилятор понимает где именно искать правильный оператор?

Ответ на этот вопрос был предложен Эндрю Кёнигом в начале 1990-х годов и сегодня мы знаем его как argument-dependent lookup, или ADL, или поиск по аргументам, заставляя компилятор искать функцию в пространствах имён, связанных с типами аргументов вызова. Но делать такой неквалифицированный поиск он будет только, если не нашёл ничего подходящего в текущей области поиска, что тоже добавляет головной боли и разработчикам компиляторов и разработчикам эти компиляторы использующим. 

namespace N {
    struct A {};
    void f(A);
}

int main() {
    N::A a;
    f(a);
}

При вызове f(a) неквалифицированный поиск имени f в текущей области ничего не находит, тем самым разрешая ADL. Тогда компилятор видит, что аргумент a имеет тип N::A, а значит видит и всё связанное с этой переменной  пространство имён N, далее ищет f внутри N, находя наконец N::f. Так вызов f  становится корректным.

Поэтому работает и поиск вывода в потоки, компилятор через аргумент-зависимый поиск (ADL) видит, что левый аргумент std::cout имеет тип, определённый в пространстве имён std, и ищет подходящий operator<< именно там. К сожалению правила у ADL запускаются только в том случае, если неквалифицированный поиск не нашёл вообще ничего, и если неквалифицированный поиск нашёл какую угодно сущность, будь то тип, переменную, шаблон, что угодно, тогда ADL полностью отключается.

typedef int f;

namespace N {
    struct A {};
    void f(A);
}

int main() {
    N::A a;
    f(a);
}

Здесь неквалифицированный поиск имени f сразу находит typedef int f и считается успешным, т.е. дальше не ищем. Соответственно компилятор интерпретирует f(a) как функциональную форму приведения типа, то есть попытку привести a к int. И это совершенно корректно с точки зрения синтаксиса, но явно не то, что ожидал программист. Именно поэтому взаимодействие обычного неквалифицированного поиска и ADL иногда приводит к крайне неожиданным результатам, когда «очевидная» функция оказывается полностью невидимой для компилятора.

Вот еще один скользкий пример (правда выглядит безопасно?):

namespace std {
    struct ostream {
        ostream& operator<<(const char*) {
            printf("world");
            return *this;
        }
    };

    ostream cout;
}

using namespace std;
int main() {
    cout << "hello";
}

Здесь имя std найдено неквалифицированным поиском и относится вовсе не к стандартной библиотеке, а к нашей локальной подмене, тогда как запись ::std::cout однозначно указывает на глобальное пространство имён и на настоящий std.

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

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

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

Проверка пригодности функции

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

// Функция с двумя параметрами
void greet(const std::string& firstName, const std::string& lastName) {
   cout << "Hello, " << firstName << " " << lastName << "!";
}

int main() {
    std::string name = "Alice";

    // Попытка вызвать функцию с одним аргументом
    // greet(name); // Ошибка: нет подходящей перегрузки

    // Правильный вызов с двумя аргументами
    greet(name, "Smith"); // Работает: Hello, Alice Smith!

    return 0;
}

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

class Person {
private:
    void secretGreet() {
        std::cout << "This is a secret greeting!" << std::endl;
    }

public:
    void publicGreet() {
        std::cout << "Hello!" << std::endl;
    }
};

int main() {
    Person p;

    // p.secretGreet(); // Ошибка: метод недоступен
    p.publicGreet();     // Работает: метод public

    return 0;
}

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

class MyString {
public:
    explicit MyString(const char* s) { /* ... */ }
};

void print(MyString s) { /* ... */ }

int main() {
    MyString a("hello");        // OK: прямая инициализация, кандидат пригоден
    MyString b = "hello";       // Ошибка: копирующая инициализация,
                                // explicit-конструктор исключён 
                                // из кандидатов
    print("hello");             // Ошибка: неявное преобразование 
                                // запрещено
    print(MyString("hello"));   // OK: явное преобразование
}

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

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

struct S {
    S(int x) { std::cout << "S(int) constructor called\n"; } 
    // пользовательское преобразование
};

void foo(double d) {
    std::cout << "foo(double) called\n";
}

void foo(S s) {
    std::cout << "foo(S) called\n";
}

void foo(...) {
    std::cout << "foo(...) called\n";
    // вариадик версия
}

int main() {
    int x = 42;

    // Какой foo будет вызван?
    foo(x);

    return 0;
}

https://godbolt.org/z/9djzdheMK

Разберем пример выше подробнее, у нас есть три перегрузки функции foo: 

  • первая принимает double и для вызова с аргументом типа int здесь будет применено стандартное преобразование int → double.

  • Вторая перегрузка принимает объект типа S, где существует конструктор S(int) и в этом случае необходимо пользовательское преобразование из int в S.

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

Теперь вызываем foo(x), где x имеет тип int. Компилятор соберет все возможные перегрузки, и на все три перегрузки подходят и каждая из них может быть вызвана с аргументом типа int, пусть и через разные преобразования.

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

  • Для foo(double) надо применить стандартное преобразование int → double, т.е. надо сделать минимальные преобразования, условно оно имеет сложность 1.

  • Для foo(S) придется использовать пользовательское преобразование через конструктор S(int), оно немного сложнее, и имеет сложность 2.

  • Для вариадической версии foo(...) будет применено вариадическое преобразование, которые задействуют самую сложную логику, и для этого придется делать много работать со сложностью 3.

«Сложность 1, 2, 3» - это мое упрощение для примере. В стандарте это называется conversion rank (ранг преобразования), и ранги не «складываются как сложность». У вас после такого изложения может сложиться впечатление, что компилятор как будто складывает баллы, но на самом деле в реальных компиляторах происходит сравнение последовательностей преобразований, по правилу «лучший по худшему параметру». И у каждого компилятора свои приколы с этими правилами.

Почему выбран именно такой порядок? Исторически это связано с эволюцией самого языка C++ и опытом его использования в ранних проектах на C, где строгие правила типов уже устоялись. Считается, что идея принадлежит Бьярне и команде ранней разработки C++ в Bell Labs, чтобы снизить сложность вычислений на машинах того времени, когда они проектировали C++. Думаю вы в очередной раз убедились, что многие современные решения выросли из 80-90-х, когда язык активно вбирал в себя новые идеи, но был ограничен производительностью железа. Так удалось достичь того, что компилятор выбирает наиболее «естественный» вариант вызова функции, не прибегая к сложным преобразованиям, если это возможно.

И еще немного исторического эксурса...

Early C++ (Cfront, 1980-е): первый компилятор C++ использовал пошаговое исключение кандидатов и сначала проверялись точные совпадения типов, потом применялись стандартные преобразования, затем пользовательские и этот процесс был полностью явным в коде компилятора и основан на ранжировании преобразований в самом копиляторе.

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

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

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

void foo(int x) {
    std::cout << "foo(int) called\n";
}

void foo(double x) {
    std::cout << "foo(double) called\n";
}

int main() {
    char c = 'A';

    foo(c); // Какой вариант будет вызван?
}

https://godbolt.org/z/x5xznr9G1

В этом примере у нас есть два кандидата: foo(int) и foo(double). Аргумент имеет тип char и компилятор может преобразовать char в int (это небольшое стандартное преобразование), оно предпочтительнее. Но также компилятор может преобразовать char в double (это тоже стандартное преобразование), но с более низким приоритетом, потому что оно требует расширения сначала в int, затем в double. В результате будет вызван foo(int), потому что преобразование char → int более «естественное» и простое, чем char → double.

Опять же для статьи я упростил подранги стандартных преобразований. В стандарте char → int будет integral promotion (продвижение, ранг exact match-ish), а char → double уже floating-integral conversion (полноценное преобразование, рангом ниже). promotion и conversion - это разные категории, и promotion всегда побеждает conversion.

Другой пример с перегрузками и значениями по умолчанию: (https://godbolt.org/z/1dbvsEezE)

void bar(int x, int y = 10) {
    std::cout << "bar(int, int) called\n";
}

void bar(double x) {
    std::cout << "bar(double) called\n";
}

int main() {
    int a = 5;

    bar(a); // Какой вариант вызовет компилятор?
}

Здесь у нас опять есть два кандидата: bar(int, int), но второй параметр имеет значение по умолчанию, поэтому вызов bar(a) подходит, и есть bar(double) где можно преобразовать int → double.

Компилятор считает приоритеты: bar(int, int) не требует преобразования (int → int) и использует значение по умолчанию для второго аргумента, тогда как bar(double) требует неявного стандартного преобразования (int → double), поэтому будет вызван bar(int, int).

=delete

=deleteпросится в статью и удалённые функции участвуют в overload resolution как пригодные кандидаты и могут быть выбраны как лучшие, после чего вызов отвергается. Это самый яркий случай, когда «пригодность» и «доступность» расходятся, и хорошо иллюстрирует, зачем понятие viability вообще нужно.

Когда мы помечаем функцию через = delete, многие воспринимают это интуитивно как «функции больше нет» и думают что-то вроде её удаления из кода. На самом деле всё ровно наоборот и удалённая функция полностью участвует в проверке пригодности и в ранжировании, ведёт себя как обычный кандидат, и компилятор может даже выбрать её как лучшую перегрузку.

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

https://godbolt.org/z/137nKavoz

void process(int x)    { std::cout << "int\n"; }
void process(double x) = delete;

int main() {
    process(42);    // OK: вызывается process(int)
    process(3.14);  // ошибка: выбран process(double),
                    //         но он удалён
    process(1.0f);  // ошибка: float → double точнее, чем float → int,
                    //         поэтому выбран удалённый process(double)
}

На первый взгляд кажется, что раз process(double) «удалён», то при вызове process(3.14) компилятор должен просто его проигнорировать и поискать другую перегрузку, например, преобразовать double в int. Но он этого делать не будет и удалённая перегрузка остаётся пригодным кандидатом, выигрывает разрешение по обычным правилам ранжирования (для double точное совпадение лучше, чем преобразование в int), и только потом упирается в = delete.

Перегрузка process(int), которая могла бы принять double через стандартное преобразование, даже не рассматривается она была бы выбрана только если бы process(double) вообще отсутствовала в списке кандидатов. Это даёт идиоматический приём запрещать вызов функции с определёнными типами, не убирая её из перегрузки, а наоборот, оставляя там специально, чтобы она «перехватывала» нежелательные аргументы

struct Handle {
    void set(int id);
    void set(void*) = delete;   // запрещаем передавать сырые указатели,
                                // включая nullptr и NULL
};

Handle h;
h.set(42);       // OK
h.set(nullptr);  // ошибка компиляции, а не неявное приведение к int

Если бы = delete означал «удаление функции из рассмотрения», то h.set(nullptr) вызвал бы set(int) через преобразование nullptr → 0, и мы получили бы трудноуловимый баг в рантайме.

Но поскольку set(void*) остаётся пригодным кандидатом и выигрывает разрешение (для nullptr указатель это будет точное совпадение, а преобразование в int нет), компилятор ловит ошибку на этапе компиляции. Именно поэтому формально различать «пригоден» и «доступен» является отдельным инструмент внутри языка, на котором держится целый класс защитных техник современного C++.

Что это все значит для разработчика

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

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

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

З.Ы. Первые 7 глав "Нескучного прогрммирования" выложил на гитхабе в ru/en (https://github.com/dalerank/playful_programming_cpp), хотя вот @OlegSivchenkoпредлагает назвать серию "Pragmatic C++", слишком уж практические и скучные вопросы поднимаются. Если будет желание поправить текст или примеры кода, напишите в личку.