es
Feedback
C++: Хроники Дурки🚑

C++: Хроники Дурки🚑

Ir al canal en Telegram

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

Mostrar más
915
Suscriptores
+324 horas
+57 días
+430 días
Archivo de publicaciones
Если кто не знаком с библиотекой nlohmann/json - она прекрасна. Мои мысли о том, как должна выглядеть библиотека для пользователя, она начала формировать еще году в 2012. (я гне нашел подтверждений, что она тогда уже была, но по ощущениям где-то тогда и появилась) Но речь не об этом. Как написано в cppreference, если ключи равны, то и хеши от них равны. ``` Given the following values: h, an object of type std::hash<Key>. k1 and k2, objects of type Key. All following requirements are satisfied: - If k1 == k2 is trueh(k1) == h(k2) is also true. - Unless std::hash<Key> is a program-defined specializationh(k1) will never throw an exception. ``` Иногда срабатывает даже если это объекты разных типов

    const auto x1 = 42;
    const auto x2 = 42u;

    __builtin_printf("%b\n\n", x1 == x2);
    __builtin_printf("%#08zx\n%#08zx\n",
        std::hash<int>{}(x1),
        std::hash<unsigned>{}(x2));
1

0x00002a
0x00002a
А вот если вы упакуете те же типы в обертку типа json:

    const auto j1 = json{x1};
    const auto j2 = json{x2};


    __builtin_printf("%b\n\n", j1 == j2);
    __builtin_printf("%#08zx\n%#08zx\n",
        std::hash<json>{}(j1),
        std::hash<json>{}(j2));
Тут уже как повезет. Конкретно для этой библиотеке не сраслось:
1

0x286fec1b50
0x286fec1b91
Потому что там есть своя специализация. И с ней это правило для hash не работает.
namespace std // NOLINT(cert-dcl58-cpp)
{

/// @brief hash value for JSON objects
/// @sa https://json.nlohmann.me/api/basic_json/std_hash/
NLOHMANN_BASIC_JSON_TPL_DECLARATION
struct hash<nlohmann::NLOHMANN_BASIC_JSON_TPL> // NOLINT(cert-dcl58-cpp)
{
    std::size_t operator()(const nlohmann::NLOHMANN_BASIC_JSON_TPL& j) const
    {
        return nlohmann::detail::hash(j);
    }
};
А потому эту штуку очень весело бывает использовать в hash-мапах, когда два одинаковых объекта могут стать разными ключами. Сохраняем бдительность!!!!

#толькосвоимемы Код взят тут. if (dim == 0) idx_dim = i0; else if (dim == 1) idx_dim = i1; else if (dim == 2) idx_dim = i2; e
#толькосвоимемы Код взят тут.

if (dim == 0) idx_dim = i0;
else if (dim == 1) idx_dim = i1;
else if (dim == 2) idx_dim = i2;
else if (dim == 3) idx_dim = i3;

const struct htp_tensor * src = (idx_dim < src0->ne[dim]) ? src0 : src1;

uint32_t s0 = i0;
uint32_t s1 = i1;
uint32_t s2 = i2;
uint32_t s3 = i3;

if (dim == 0 && src == src1) s0 -= src0->ne[0];
if (dim == 1 && src == src1) s1 -= src0->ne[1];
if (dim == 2 && src == src1) s2 -= src0->ne[2];
if (dim == 3 && src == src1) s3 -= src0->ne[3];
Вообще по библиотеке много кода, который заставляет задумываться о вечном.

Ладно, искать баги в компиляторах весело, но недостаточно. Давайте поиграемся в чуть более интересные игры. Вспомним, что у нас в С++ есть правило: структура не может иметь нулевой размер. Потому что у ее инстанса должен быть адрес, и здесь С++ как-то забыл про Zero Cost и все такое. Зато С++ вспомнил про Zero Cost, когда мы примешиваем пустую структуру в наследование другой.
struct Empty1 {};
struct Empty2 {};

struct NonEmpty {
    int Value;
};

struct MyClass1
    :public NonEmpty
    ,public Empty1
    ,public Empty2
{
};

struct MyClass2
    :public Empty1
    ,public NonEmpty
    ,public Empty2
{
};

struct MyClass3
    :public Empty1
    ,public Empty2
    ,public NonEmpty
{
};


static_assert(sizeof(NonEmpty) + sizeof(Empty1) + sizeof(Empty2) == 6);

static_assert(sizeof(int) == 4);
static_assert(sizeof(MyClass1) == 4);
static_assert(sizeof(MyClass2) == 4);
static_assert(sizeof(MyClass3) == 4);

int main() {}
Вот эта штука скомпилируется. Тоесть у нас структуры без размера имеют размер, но этот размер не добавляется к размеру общей структуры. Всвязи с чем я упоролся, и написал вот такой класс:
struct Empty {
    void zero() { std::memset(this, 0, sizeof(*this)); }
};
Свиду ничем не примечательный класс, который просто заполняет себя нулями. И это нормально работает для непустого класса, но пустой класс такими выкрутасами ломает соседние поля в классе, который его отнаследовал.
struct CompressedPair : Empty {
    CompressedPair(Empty e, bool x) : Empty(e), b(x) { }
    bool b;
};


int main() {
    auto a = CompressedPair({}, true);

    __builtin_printf("%b\n", a.b);
    a.zero(); 
    __builtin_printf("%b\n", a.b);

    return 0;
}
Выводит
1
0
(да, да, это UB, я знаю, прекратите. Мы тут поржать собрались или где?) Но где-то тут в безумии зараждается идея... У нас же есть no_unique_address. А на gcc он кладет данные в padding других структур.
#include <iostream>

template <typename T>
struct optional {
    union {
        char foo;
        T val;
    };
    [[no_unique_address]] bool engaged;
};

template <typename View>
struct reverse_view {
    [[no_unique_address]] optional<typename View::iterator> cache;
    View base_;
};

struct MyView {
    using iterator = int*;
    char begin_;
};

int main() {
    std::cout << "sizeof = " << sizeof(reverse_view<MyView>) << std::endl; // clang - 24, gcc - 16
    std::cout << "alignof = " << alignof(reverse_view<MyView>) << std::endl;
}
И тут я очень хотел развернуться мыслью, но быстро нашел уже закрытый баг с std::expected. (половина примеров выше взята из обсуждения этого бага, потому что они нагляднее чем то, с чем работал я). Если коротко, то методы expected могли зачищать переменные вокруг себя, если те оказывались в из padding. Потому что emplace пересоздавал внутренний объект целиком. Но, увы, примера воспроизводящего это дело у меня нет. А после этого примера все остальное рядом меркнет. Увы.

Ладно, разумеется прошлый пост был набросом. Никогда не обновляйте компиляторы без очень серьезных на то причин. Мало ли что там сломается, а у вас не будет команды все это чинить. Вот к примеру. Вложенные лямбды с параметр паком.
#include <utility>

template <std::size_t N, std::size_t M>
concept C = requires {
    []<std::size_t... Is>(std::index_sequence<Is...>)
  requires requires {
      []<std::size_t... Js>(std::index_sequence<Js...>)
    requires requires { true; }
      {}(std::make_index_sequence<M>{});
  }
    {}(std::make_index_sequence<N>{});
};

static_assert(C<2, 3>);
В clang 17 они нормально компилируются. В clang 20 они крашат компилятор. В clang 22 это починили и они компилируются. В текущем транке снова ломают компилятор. Не выходи из комнаты, не совершай ошибку. Компилятор обновляют слабаки и трусы.

Убедительная просьба - обновляйте компиляторы. Помимо внедрения множества способов выстрелить себе в ногу, новые версии компиляторов чинят разные веселые баги. Вот к примеру вот этот код:
struct S {
  int x;

  auto foo() {
    return [*this](this auto&& self) {
      __builtin_printf("%d ", x);
      x = 10;
    };
  }
};
Возвращаемая лямбда возвращаемая из метода foo капчурит this, и потом модифицирует значение. Но что будет, если метод будет вызван у константного класса?
int main() {
  S s{ 5 };
  const auto l = s.foo();
  l();
  s.x = 15;
  l();
  __builtin_printf("%d\n", s.x);
}
вот тут clang18.1 нормально сработает и модифицирует константный класс. clang19.1 выдаст ошибку компиляции. А gcc15.2 упадет с segfault https://godbolt.org/z/8qqbehqhE

Я сначала подумал, что надо на такие мероприятия футболку брать с QR кодом канала на спине, пиарить канал на конфе. А потом п
Я сначала подумал, что надо на такие мероприятия футболку брать с QR кодом канала на спине, пиарить канал на конфе. А потом подумал: меня же спалят как админа этого канала... Свят-свят-свят. P.S. новый пост зашедулен на завтра :)

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

int main() {
    std::map<int, std::unique_ptr<int>> m;

    m.emplace(1, std::make_unique<int>(42));

    auto p = std::make_unique<int>(100);

    auto [it, inserted] = m.emplace(std::make_pair(1, std::move(p)));
}
Здесь классика классика. Мапа из чего-то в в unique_ptr. Ну кто так не делал (здесь должна быть ссылке на open-source пример, но у меня жопа горит). Дальше мы делаем emplace.
Return value
A pair consisting of an iterator to the inserted element (or to the element that prevented the insertion) and a bool value set to true if and only if the insertion took place.
Ну и вот второй emplace с ключом 1 вернет false. А что же произойдет с p? Он занулится, потому что мы его мувнули в функцию.

int main() {
    std::map<int, std::unique_ptr<int>> m;

    m.emplace(1, std::make_unique<int>(42));

    auto p = std::make_unique<int>(100);

    auto [it, inserted] = m.emplace(std::make_pair(1, std::move(p)));

    std::cout << std::boolalpha;
    std::cout << "inserted = " << inserted << '\n';
    std::cout << "p is null = " << (p == nullptr) << '\n';
    std::cout << "*it->second = " << *it->second << '\n';
}
Выводит
inserted = false
p is null = true
*it->second = 42
Тоесть в мапу значение не поместили, а из указателя оно пропало... Восхитительно. Притом что если делать std::move "на месте" такого не происходит.

int main() {

    auto p = std::make_unique<int>(100);

    std::move(p);

    std::cout << std::boolalpha;
    std::cout << "p is null = " << (p.get() == nullptr) << '\n';
}

p is null = false
Да, это чинится тем, что мы убираем make_pair, но это в тестовом примере, где это легко заметить. И когда уже знаешь проблему. Я задолбался ловить такие приколы с move.

Приятно видеть, что C++ настолько тщательно следит за обратной совместимостью. Мы столько боли ради этой обратной совместимости пережили. И теперь вдвойне приятно видеть, что поменяться может семантика вообще всего что угодно. Например, вот это:

decltype(auto) f(int&& x)
{
    return (x);
}

static_assert(std::same_as<decltype(f(1)), int&>);

Оно нормально скомпилируется в С++20, но упадет на static_assert в C++23. Тоесть не просто перестанет компилироваться, а тихой сапой скомпилируется "в другое". Насколько понимаю, связано вот с этим: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2266r3.html Но меня уже разоблачили, плюсовик я не настоящий и гениальности очередного замысла могу не понять. Как хорошо, что у меня канал не образовательный а развлекательный 🙂

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

template<typename T>
struct S {
    S(T) {
        std::cout << "S(T)" << std::endl;
    }
    template<typename Iter>
    S(Iter beg, Iter end) {
        (beg); (end);
        std::cout << "S(Iter beg, Iter end)" << std::endl;
    }
};
Если мы напишем что-то такое:

    int a = 3;
    auto s1 = S(a);

    static_assert(std::same_as<decltype(s1),
            S<int>>
        );
То оно нормально скомпилируется. А вот если мы попробуем как-то тригернуть второй конструктор:

    std::vector<double> v;

    auto s2 = S(v.begin(), v.end());
То ожидаемо упадем с ошибкой, смысл которой сводится к тому, что компилятор не знает, какой именно тип выводить.
 error: no viable constructor or deduction guide for deduction of template arguments of 'S'
Что довольно логично. Но оказывается, есть возможность указать "хинт" для того, какой тип должен выводить конструктор класса!

template<typename Iter>
S(Iter, Iter) 
   -> S<typename std::iterator_traits<Iter>::value_type>;
И тогда вот это компилируется: https://godbolt.org/z/MaPo7PsP4

    std::vector<double> v;

    auto s2 = S(v.begin(), v.end());

    static_assert(std::same_as<decltype(s2),
            S<double>>
        );
У Константина дальше в докладе было всякое, какие у этого есть побочные эффекты. Посмотрите доклад потом сами. Много примеров есть прямо вот тут: https://eel.is/c%2B%2Bdraft/over.match.class.deduct А меня просто понесло. Мне так нравятся типы, которые меняются при каждом копировании, кто бы знал (и да, весь пост написан только ради этой ржаки): https://godbolt.org/z/4EMnjWsza

#include <concepts>

struct Charmander {};
struct Charmeleon {};
struct Charizard {};

template<typename Form>
struct Pokemon {
    Pokemon(Form) {}

    template<typename OtherForm>
    Pokemon(const Pokemon<OtherForm>&) {}
};


Pokemon(const Pokemon<Charmander>&)
    -> Pokemon<Charmeleon>;

Pokemon(const Pokemon<Charmeleon>&)
    -> Pokemon<Charizard>;

Pokemon(const Pokemon<Charizard>&)
    -> Pokemon<Charmander>;


int main() {
    Pokemon p1 = Charmander{};
    Pokemon p2 = p1;
    Pokemon p3 = p2;
    Pokemon p4 = p3;

    static_assert(std::same_as<
        decltype(p1),
        Pokemon<Charmander>
    >);

    static_assert(std::same_as<
        decltype(p2),
        Pokemon<Charmeleon>
    >);

    static_assert(std::same_as<
        decltype(p3),
        Pokemon<Charizard>
    >);

    static_assert(std::same_as<
        decltype(p4),
        Pokemon<Charmander>
    >);
}

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

int main() {
    Box b;
    b->hi();
}
Что тут происходит? Мы создаем объект типа Box, иииии... вызываем у него оператор ->, а у того, что он вернет вызывает функцию hi(). Логично? Логично! Смотрим, что такое у нас тип Box.

struct Box {
    Proxy operator->() {
        return {};
    }
};
Ага, значит оператор выдает экземпляр объекта типа Proxy, и вот у него вызывается функция hi(). Логично? Логично. Смотрим, что такое структура Proxy.

struct Proxy {
    Leaf* operator->() {
        static Leaf x;
        return &x;
    }
};
В структуре Proxy функции hi() нет. Ошибка компиляции. Логично? А хрен там плавал. Уберите детей от мониторов. Вот этот код компилируется и выводит ok

#include <iostream>

struct Leaf {
    void hi() { std::cout << "ok"; }
};

struct Proxy {
    Leaf* operator->() {
        static Leaf x;
        return &x;
    }
};

struct Box {
    Proxy operator->() {
        return {};
    }
};

int main() {
    Box b;
    b->hi();
}
Итак. У нас С++ разворачивает оператор -> до самого конца. Например сработает даже вот это:

#include <iostream>

struct Leaf {
    void hi() { std::cout << "ok"; }
};

struct Proxy4 {
    Leaf* operator->() {
        static Leaf x;
        return &x;
    }
};

struct Proxy3 {
    Proxy4 operator->() {
        return {};
    }
};

struct Proxy2 {
    Proxy3 operator->() {
        return {};
    }
};

struct Proxy {
    Proxy2 operator->() {
        return {};
    }
};

struct Box {
    Proxy operator->() {
        return {};
    }
};

int main() {
    Box b;
    b->hi();
}
Я вот хочу куда-нибудь в шаблонный код загнать, пранкануть, таксказать, коллег.

Мой дорогой друг вот с этого канала о С++: @thisnotes Прислал мне просто восхитительную вещь. Давайте посмотрим вот на этот код

const std::vector<Obj>& vec = 
    (argc > 1)
    ? std::vector<Obj>{}
    : ready_vec;
Вот тут зашита просто удивительая проблема. Нет, это не ссылка на временный объект, потому что const продлевает время жизни ссылки. Интересный факт, что вот такой код

#include <iostream>
#include <vector>

struct Obj {
    explicit Obj() {}
    Obj(const Obj&) {
        std::cout << "copy\n";
    }
};

int main(int argc) {

auto ready_vec = std::vector<Obj>{};
ready_vec.reserve(1);
ready_vec.emplace_back();

const std::vector<Obj>& vec = 
    (argc > 1)
    ? std::vector<Obj>{}
    : ready_vec;

    return vec.size();
}
Компилируется в 16 строк ассемблера, а вот если мы сделаем вот так:

#include <iostream>
#include <vector>

struct Obj {
    explicit Obj() {}
    Obj(const Obj&) {
        std::cout << "copy\n";
    }
};

int main(int argc) {

auto ready_vec = std::vector<Obj>{};
ready_vec.reserve(1);
ready_vec.emplace_back();

const auto empty_vec = std::vector<Obj>{};
const std::vector<Obj>& vec = 
    (argc > 1)
    ? empty_vec
    : ready_vec;

    return vec.size();
}
Компилируется в 6 строк ассемблерного кода. А фокус в том, что тернарный оператор в этой строчке

const std::vector<Obj>& vec = 
    (argc > 1)
    ? std::vector<Obj>{}
    : ready_vec;
должен оба аргумента конвертировать к одному типу. А временный объект к ссылке сконвертировать не получится, поэтому и второй аргумент к ссылке не может быть конвертирован, и создается временная копия вектора. Думаю, в комментариях он подскажет, был ли этот пример найден в продуктовой задаче...

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

int fn(int a, int b) {
  int res = 0;
  asm volatile ("nop");
  asm volatile ("nop");
  asm volatile ("nop");
  asm volatile ("nop");
  if (a > b) { // проблемный бренч
    res = std::rand();
  }
  res = std::max(res, a);
  return res;
}
Утверждается, что это пример из реальной жизни, а постановка четырех NoOperation смещает операцию так, что branch prediction работает сильно лучше и значительно улучшает производительность. Вот такая она оптимизация в 2026 году.... P.S. Если я кому-то кидал текст изначального поста - дайте знать. Там было сильно больше интересного, но больно уж мне этот пример понравился. P.P.S. А вообще его доклад (и другие доклады) рекомендую к просмотру хотябы из чуства мазохизма. Если у кого-то есть иллюзии, что он шарит в IT - самое время разочароваться в собственных знаниях :)

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

В разных новых (уже скоро будет 10 лет) стандартах иногда бывают мелкие но очень приятные изменения. Вот такой код
#include <iostream>

int main() {
    bool b = true;

    b++;

    std::cout << b;
}
Был валиден до С++17, но
<source>:6:5: error: use of an operand of type 'bool' in 'operator++' is forbidden in C++17
Вот так вот.

Вот сколько я дурки повидал (у меня канал про это целый заведен так-то !!!) но с удивлением я осознал, что вот такая штука

%:include <iostream>

int main() <%
    int a<:3:> = <% 10, 20, 30 %>;
    std::cout << a<:1:>;
%>
Компилируется во всех комипляторах... Потому что язык заботливо сохранил диграфы — на случай, если ваша клавиатура из 1973 года. Пипец какая жесть.

Разбираем письма читателей. Нам прислали вот такой вот код:

#include <memory>
#include <iostream>

namespace user {

struct user_type {};

using my_type = std::shared_ptr<user_type>;

void tie(my_type const&, my_type const&)
{
    std::cout << "user::tie\n";
}

void oups()
{
    my_type t1;
    my_type t2;

    tie(t1, t2);
}

} // namespace user

int main()
{
    user::oups();
}
Что в нем примечательного. Под gcc/clang у нас в консоль ничего не запишется.
Program returned: 0
Для MSVC x64 запишется
Program returned: 0
user::tie
А для MSVC x86 вообще случится страшное:
Program returned: 3221225595
Ну и чтобы не оставлять предложку совсем уж без изменений, я добавлю от себя немного дурки. Если в вызов функции добавить скобки:

void oups()
{
    my_type t1;
    my_type t2;

    (tie)(t1, t2);
}
То, внезапно, в gcc/clang мы тоже будем печатать строчку. А вот в MSVC x86 все еще будет возвращаться ненеулевой код....

Сегодняшняя рубрика называется "обычный шаблонный код, который компилируется только после жертвоприношения". Что выведет вот этот код?
cpp 
#include <iostream>
#include <vector>

template <class T>
struct LoggedVector : std::vector<T> {
    void Dump() const {
        if (empty()) {
            std::cout << "empty\n";
            return;
        }
        std::cout << "size = " << size() << '\n';
    }
};

int main() {
    LoggedVector<int> v;
    v.Dump();
}

Иииииии... Правильный отвееееет..... Да, вы правы, он ничего не выведет. Упадет на ошибке компиляции: ``` error: use of undeclared identifier 'empty' ``` Небольшой отступ чтобы код под спойлером не бросался в глаза Что тут не так. На самом деле надо делать или так:

    void Dump() const {
        if (this->empty()) {
            std::cout << "empty\n";
            return;
        }
        std::cout << "size = " << this->size() << '\n';
    }
Или вот так:

    using std::vector<T>::size;
    using std::vector<T>::empty;
Ты literally видишь перед собой size() и empty(), они вот там, в базовом классе, рукой подать. Но компилятор такой: Нет. В шаблонах я сначала притворяюсь, что базового класса почти не существует. Особенно приятно при большом рефакторинге, когда меняешь не-шаблонный класс на шаблонный, а он потом в произвольных местах кода ломается...

Не могу не поделиться самым веселым, на мой взгляд, примером из доклада великолепного Константина Владимирова. Он делал анонс доклада в своем tg канале. Пример вот такой:

template <auto T = []{}> 
struct S {};

S a; S b;
В чем тут цимес. У нас T - это лямбда. И если мы таким образом определяем переменные, то в тип S записываются разные лямбды, и у нас получаются два разных типа:

static_assert(
    !std::is_same_v<decltype(a), decltype (b)>
);
Если же мы явно укажем пустые треугольные скобки вот так:

S<> a, b;

static_assert(
    std::is_same_v<decltype(a), decltype (b)>
);
То типы, внезапно, станут одинаковыми. Ну, мы один раз объявили тип, и две переменные этого типа. А теперь вопрос в зал. А что если мы определим эти две переменные точно так же, как во втором варианте, но только без явного указания треугольных скобок?

S a, b;
Давайте вы попробуете угадать? Ставя треугольные скобки мы исключаем вывод типов. Мы явно указываем, какой тип мы используем. Но если у нас есть вывод типов, у нас компиляторы начинают вести себя по-разному. clang падает с ошибкой ``` error: template arguments deduced as 'S<(lambda at <source>:4:20){}>' in declaration of 'a' and deduced as 'S<(lambda at <source>:4:20){}>' in declaration of 'b' 7 | S a, b; ``` А gcc считает, что это два разных типа. ``` static_assert( !std::is_same_v<decltype(a), decltype (b)> ); ``` Пруф. Вцелом доклад Константина был просто прекрасным, и я, наверное, понатырю сюда еще примеров из его доклада через пару месяцев. А когда он выйдет в открытый доступ - обязательно дам ссылку. Я был просто в восторге от дурки, которую он показывал.

Сижу на CppRussia. Пока тут каждый второй слайд первого же доклада - кандидат на пост сюда....

Посмеемся?

#include <iostream>
#include <string_view>

struct Base {
    void Set(std::string_view) { std::cout << "string\n"; }
    void Set(int)              { std::cout << "int\n"; }
};

struct Derived : Base {
    void Set(bool)             { std::cout << "bool\n"; }
};

int main() {
    Derived d;
    d.Set("hello");
}
Что выведет? Ответ конечно `bool`: Почему так? Потому что перегрузки из Base в Derived скрываются целиком, если в наследнике появился метод с тем же именем. И дальше d.Set("hello") уже ищет только среди перегрузок Derived. А const char* в bool конвертируется просто замечательно. И по нашей любимой традиции - ни одного ворнинга ни в одном из компиляторов.