Подтвердить что ты не робот

Безопасно ли использовать адрес статической локальной переменной в шаблоне функции как идентификатор типа?

Я хочу создать альтернативу std::type_index, которая не требует RTTI:

template <typename T>
int* type_id() {
    static int x;
    return &x;
}

Обратите внимание, что адрес локальной переменной x используется как идентификатор типа, а не значение x. Кроме того, я не намерен использовать голый указатель в реальности. Я просто убрал все, что не касается моего вопроса. См. Текущую реализацию type_index здесь.

Этот подход звучит, и если да, то почему? Если нет, почему бы и нет? Я чувствую, что я нахожусь на шаткой почве здесь, поэтому меня интересуют точные причины, по которым мой подход будет или не будет работать.

Типичным примером использования может быть регистрация подпрограмм во время выполнения для обработки объектов разных типов через один интерфейс:

class processor {
public:
    template <typename T, typename Handler>
    void register_handler(Handler handler) {
        handlers[type_id<T>()] = [handler](void const* v) {
            handler(*static_cast<T const*>(v));
        };
    }

    template <typename T>
    void process(T const& t) {
        auto it = handlers.find(type_id<T>());
        if (it != handlers.end()) {
            it->second(&t);
        } else {
            throw std::runtime_error("handler not registered");
        }
    }

private:
    std::map<int*, std::function<void (void const*)>> handlers;
};

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

processor p;

p.register_handler<int>([](int const& i) {
    std::cout << "int: " << i << "\n";
});
p.register_handler<float>([](float const& f) {
    std::cout << "float: " << f << "\n";
});

try {
    p.process(42);
    p.process(3.14f);
    p.process(true);
} catch (std::runtime_error& ex) {
    std::cout << "error: " << ex.what() << "\n";
}

Заключение

Спасибо всем за вашу помощь. Я принял ответ от @StoryTeller, поскольку он изложил, почему решение должно быть действительным в соответствии с правилами С++. Тем не менее, @SergeBallesta и ряд других в комментариях указали, что MSVC выполняет оптимизацию, которая неудобно близка к нарушению этого подхода. Если требуется более надежный подход, то предпочтительным может быть решение с использованием std::atomic, как это было предложено в @galinette:

std::atomic_size_t type_id_counter = 0;

template <typename T>
std::size_t type_id() {
    static std::size_t const x = type_id_counter++;
    return x;
}

Если у кого-то есть другие мысли или информация, я все еще хочу его услышать!

4b9b3361

Ответ 1

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

Итак, в каждой единицы перевода вы получите адрес той же статической локальной переменной для вызова type_id<Type>(). Вы защищены здесь от нарушений ODR стандартом.

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

Ответ 2

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

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

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

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

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

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

int unique_val() {
    static int cur = 0;  // normally useless but more readable
    return cur++;
}
template <typename T>
void * type_id() {
    static int x = unique_val();
    return &x;
}

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

Примечание: я думаю, что, поскольку значение не будет использоваться, возврат void * будет звучать чище...


Просто добавление, украденное из комментария от @bogdan. Известно, что MSVC имеет очень агрессивную оптимизацию с флагом /OPT:ICF. Обсуждение предполагает, что это не должно быть согласованным и что оно применимо только к переменной, обозначенной как const. Но это подтверждает мое мнение, что даже если код OP кажется совместимым, я бы не рискнул использовать его без дополнительных мер предосторожности в производственном коде.

Ответ 3

Редактирование после комментария. Я не понял, сначала, что адрес использовался как ключ, а не значение int. Это умный подход, но он страдает от ИМХО - главный недостаток: намерение очень неясное, если кто-то еще находит этот код.

Похоже на старый C-хак. Это умный, эффективный, но код не объясняет вообще, что это за намерение. Что в современном С++, imho, плохо. Напишите код для программистов, а не для компиляторов. Если вы не доказали, что существует серьезное узкое место, которое требует оптимизации голого металла.

Я бы сказал, что он должен работать, но я явно не юрист языка...

Элегантное, но сложное решение constexpr можно найти здесь или здесь

Оригинальный ответ

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

Но:

  • Зачем возвращать указатель non const? Это позволит вызывающим абонентам изменять значение статической переменной, что явно не то, что вы хотели бы
  • Если возвращать указатель const, я не вижу интереса к тому, чтобы не возвращать значение вместо возврата указателя

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

Как вы инициализируете статические значения int? Здесь вы не инициализируете их, чтобы это было неверно. Возможно, вы хотели использовать указатель non const для их инициализации где-то?

Есть две лучшие возможности:

1) Специализируйте шаблон для всех типов, которые вы хотите поддерживать

template <typename T>
int type_id() {
    static const int id = typeInitCounter++;
    return id;
}

template <>
int type_id<char>() {
    static const int id = 0;
    return id;  //or : return 0
}

template <>
int type_id<unsigned int>() {
    static const int id = 1;
    return id;  //or : return 1
}

//etc...

2) Используйте глобальный счетчик

std::atomic<int> typeInitCounter = 0;

template <typename T>
int type_id() {
    static const int id = typeInitCounter++;
    return id;
}

Этот последний подход ИМХО лучше, потому что вам не нужно управлять типами. И, как указано A.S.H, счетчик с нарастающим счетом с нулевым значением позволяет использовать vector вместо map, который намного проще и эффективнее.

Кроме того, используйте unordered_map вместо map для этого, вам не нужно упорядочивать. Это дает вам O (1) доступ вместо O (log (n))

Ответ 4

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

template<int *>
struct S {};

//...

S<type_id<char>()> s;

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

Если вы можете справиться с этими ограничениями, это просто отлично.


Если вы уже знаете типы, для которых требуется постоянный идентификатор, вы можете использовать что-то вроде этого (в С++ 14):

template<typename T>
struct wrapper {
    using type = T;
    constexpr wrapper(std::size_t N): N{N} {}
    const std::size_t N;
};

template<typename... T>
struct identifier: wrapper<T>... {
    template<std::size_t... I>
    constexpr identifier(std::index_sequence<I...>): wrapper<T>{I}... {}

    template<typename U>
    constexpr std::size_t get() const { return wrapper<U>::N; }
};

template<typename... T>
constexpr identifier<T...> ID = identifier<T...>{std::make_index_sequence<sizeof...(T)>{}};

И создаст ваши идентификаторы следующим образом:

constexpr auto id = ID<int, char>;

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

handlers[id.get<T>()] = ...

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

template<std::size_t>
struct S {};

// ...

S<id.get<B>()> s{};

В операторе switch:

    switch(value) {
    case id.get<char>():
         // ....
         break;
    case id.get<int>():
        // ...
        break;
    }
}

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

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