Comments 18
Не совсем понял проблему. "Я хочу, чтобы все, кто инклудит вот этот файл, были бы обязаны вызвать вот такую функцию с каким-то своим аргументом". Зачем? Ваш файл содержит другие функции, которые будут вызваны. Нельзя ли сделать так, чтобы в первую вызываемую реально полезную функцию передавался этот аргумент?
Беседы с ИИ пересказывать, пожалуйста, не надо. И запятые, блин, научитесь их ставить!
Нельзя ли сделать так, чтобы в первую вызываемую реально полезную функцию передавался этот аргумент?
первая вызываемая функция это конструктор класса Sensor, как базового класса, например вот здесь: https://github.com/openbmc/dbus-sensors/blob/e09c58c3a6949f58091f24a59883f93932552c72/src/fan/FanMain.cpp#L632
Передавать в каждый создаваемый объект имя единственного файла для всего демона, согласитесь, было бы не менее странным решением, хотя это конечно возможно.
Беседы с ИИ пересказывать, пожалуйста, не надо. И запятые, блин, научитесь их ставить
Взаимоисключающие параграфы©
Казалось бы, современный C++ дает нам море инструментов для контроля кода на этапе компиляции: static_assert, концепты, constexpr всё, что только можно.
Вот именно: казалось бы. Я одно время увлёкся consteval, потому что люблю препроцессить. Но очень быстро обломился. Мало того, что он сырой и обрезанный, и толком на нём не развернёшься, так ещё и плохо продуманный. Как раз в вопросах контроля кода. Мне попался однажды чужой фрагмент, где consteval-функция делала throw "Invalid blahblahblah";. Я подумал: ага, эта семантика мне знакома. Так, например, делает LESS/WebCompiler 2022+. Кидаем строку под видом исключения в compile-time — видим эту строку в списке ошибок компиляции. Щаззз! Сообщение при компиляции было: «Бида-бида, вылетело исключение, а почему — сам разберись». Почему нельзя было в стандарте закрепить требование превращать такие строки в ошибки компиляции?
Ладно, стал искать, чем заменить. Посоветовали попробовать static_assert. Ну, я попробовал и теперь имею вопрос: его вообще в принципе можно заставить делать что-то полезное? Потому что на практике ты пишешь какой-нибудь высокоуровневый валидатор, а ошибка происходит на пятом уровне вложенности, среди строительных кубиков, из которых состоят все валидаторы (DRY же). И понять, что́ именно не так, просто НЕВОЗМОЖНО.
Как всегда в таких случаях, буду рад узнать, что ошибаюсь. Что я просто не умею готовить это «море инструментов для контроля кода».
Ну вот я умею колдовать в компайл-тайме.
(И чтобы немножко себя простимулировать к дальнейшей писанине, - упомяну мой проект nenormal - исключительно чорная магия, демонстрация возможностей, не для продакшена)
Внезапно, но constexpr-функции могут содержать некоторое количество рантайма.
Другое дело, что ошибки в компайл-тайме сделать человекочитаемыми - это большое искусство.
Для внятной диагностики нужно
по возможности, всё обмазывать констрейнами
по возможности, делать констрейны на именованных концептах
по возможности, инкапсулировать многоэтажные шаблоны внутрь фиксированных типов (например, через наследование) - это убирает многословность
использовать зависимые типы - это увеличивает многословность, но при ошибке показывает значение, с которым возникла проблема; ну и вообще, повышает типизацию
по возможности, делать констрейны на именованных концептах
А вы можете показать, как сделать семантику констрейна на именованных концептах вот для этого примера? Заранее спасибо.
А то я смутно надеялся, что когда в будущем случайно передам форматтеру аргумент незнакомого типа, компилятор скажет: вот тут ты передал тип, который не пролазит через концепт. И вот это будет настоящий констрейн, как я их представляю. В реальности же я в таких ситуациях получаю error C2672: no matching overloaded function found. Что технически верно, но от этого не легче.
(На всякий случай: в остальном вопрос решён, всё работает, как планировалось, но у меня язык не поворачивается назвать это констрейном, с такой-то диагностикой).
А почему нельзя сделать сам экземпляр класса внешней ссылкой?
Можно ещё как синглтон реализовать. Функцию instance() сделать с параметром - именем файла, а если её вызвали второй раз с другим именем - или паника, или просто возвращаем уже созданный экземпляр.
GCC грешит избыточным выкидыванием кода, даже если оптимизации отключены. Clang в этом лучше.
Но в общем случае задача 100% покрытия тестами, кажется, в С++ не решаема принципиально. Особенно в случае широкого использования шаблонов.
Весьма вероятно, что volatile - ваш друг!
Компилятор не имеет права убрать присвоение volatile-переменной.
Я немного упростил код, но, надеюсь, не повредил существо дела:
Файл a.hpp:
#include <iostream>
extern bool tok;
//static inline const bool tok_received = tok;
static inline const volatile bool tok_received = tok;
inline void f(){
std::cout << "\nHello!\n";
}Файл main.cpp:
#include <iostream>
#include "a.hpp"
bool initTok(){
std::cout << "\ntoken initialized!\n";
return true;
}
// bool tok = initTok();
int main(){
f();
}g++ -O3 main.cpp - ошибка линкера (g++ 13.3.0, Ubuntu)
Если убрать volatile из объявления, ошибка линкера исчезает, инициализация не вызывается.
Если, наоборот, в main.cpp раскомментироваать определение флага tok, ошибка ичезает и инициализация вызывается.
Инициализировать что-то строго до main - может быть узким местом. А вдруг понадобится конфигурировать из комстроки?
Кажется, есть достаточно простое решение без ассемблера.
#include <iostream>
struct InitToken {};
// user-defined
void init_token();
inline InitToken g_init_token = (init_token(), InitToken{});
inline InitToken get_init_token() { return g_init_token; }
struct Sensor {
explicit Sensor(InitToken = get_init_token()) {}
};
// если не раскомментировать, то будет ошибка линкера
// void init_token() { std::cout << "init" << std::endl; }
int main() {
Sensor s;
}Интересно, что ваше решение можно упростить почти до нуля (https://godbolt.org/z/GnbxYvKq5)
#include <iostream>
// user-defined
extern void init_token();
inline bool token_initialized = (init_token(), true);
// если не раскомментировать, то будет ошибка линкера
//void init_token() { std::cout << "init" << std::endl; }
int main() {
}Судя по всему, компилятор не имеет права убрать инициализацию, имеющую явный побочный эффект (ибо справедливо считает, что меняется observable behavior программы).
Вот, кстати, демонстрация того, что inline-переменная инициализируется единожды, будучи определена в нескольких единицах трансляции.
Стандартное и правильное решение решение - через объявление внешней функции без реализации. Для классов - абстрактным методом.
Вызов перед main решается через конструктор глобальной переменной класса, через "attribute((constructor));" или через "#pragma startup my_startup_code".
А не проще отказаться от статических методов вообще?
const Sensor sensor("MySensorConfigValue");
int main() {
sensor.doWork(); // Выведет: Working with param: MySensorConfigValue
return 0;
}Простите за грубость, но статья откровенное Г. Возможно вы, как погруженный в тему, имеете представление о проблеме, а вот я, как читатель, так и не понял что именно вам нужно и почему вы этого не можете получить. Полагаю, очень не хватает иллюстрирующих суть происходящего примеров кода. Типа вот это есть, вот этого хотелось бы, вот из-за этого не получается.
Отдельно доставляют запрятанные под кат простыни диалога с ИИ. Они кому-то кроме вас интересны?
ИМХО, просто образец того, как писать статьи не нужно. Еще раз проссыте за грубость.
А я потом опубликую продолжение этого разговора в котором ИИ оправдывается-анализирует почему он не смог предложить мне, если не правильное решение
Может лучше не надо?
Уверен, что не понял суть ваших затруднений. Но сложилось впечатление, что вам нужно от класса Sensor сделать шаблонный класс-наследник. Который должен параметризоваться специальным классом с нужными вам свойствами. И в коде далее должен использоваться не Sensor, а этот шаблонный наследник. Что-то типа:
template< typename Props >
class BasicSensor : public Sensor {
...
// Этот метод должен использоваться для получения пути
// к тестовым файлам.
[[nodiscard]] std::filesystem::path getTestDataPath() {
// А вот главный трюк: это значение должен предоставлять класс Props.
return Props::testDataPath;
}
...
}
После чего для того, чтобы использовать BasicSensor пользователь должен определить соответствующий класс свойств со статическим членом внутри.
struct MySensorProps {
static std::filesystem::path testDataPath;
};
...
class MySensor : public BasicSensor< MySensorProps > {
...
};
И ничего у вас компилятор выбрасывать не будет.
Если testDataPath не понадобится, то всё компилятор отлично выкинет.
Я не очень понял конкретных страданий автора, но догадываюсь, что существует проблема протокола работы с внешним миром.
Например, перед тем, как использовать сенсоры шины, нужно сперва эту шину инициализировать. И вот хотелось бы гарантировать в компайл-тайме, что этот вызов реально сделан.
Для этого есть разные способы.
Можно прибегнуть к токенам. Функция инициализации возвращает значение типа, который можно сконструировать только в этой функции. А функции работы - да хотя бы конструкторы объектов сенсоров - принимают это значение.
Правда, этот токен надо где-то хранить.
Если в глобальной переменной, то возникает риск использовать неинициализированную переменную, причём необязательно даже это будет UB. А если принимать по ссылке и дальше не использовать, то оптимизирующий компилятор может выкинуть ненужное и избавить линкер от ошибок.
Если же в локальной переменной, то возникнет лютейший оверхед - передача её от точки создания до абсолютно всех точек использования. А у нас не хаскелл, чтобы вот так вот монаду IO эмулировать, и нам за это ничего не было бы.
Можно попытаться напрячь линкер (что и задумал автор). Но пошёл по пути токенов. А там оптимизирующий компилятор подкараулил!
Я выше показал, как можно не прибегать к ассемблерной магии.
На дворе C++23, а заставить компилятор проверять код всё ещё помогает только ассемблер