• Как использовать потоки в современном C++ в приложении на основе цикла событий?

    @code_panik
    В сущности, async - это thread + promise/future с удобствами: его не нужно join-нить руками, не нужно создавать промежуточный promise, а возвращенный функцией результат и исключения можно получить в вызывающем коде. При этом деструктор объекта future, полученного в результате вызова async, блокирует вызывающий поток до завершения нашей async-функции.
    В нашем случае разницы с использованием thread в части отправки сигналов нет, ведь нам по-прежнему придется сигнализировать виджету о завершении сканирования. Это можно сделать в самой функции сканирования. Если я правильно понял, раньше мы это делали в функции scanFinished, которая вызывалась в конце функции сканирования.
    Под задачами обычно понимают независимые потоки выполнения (необязательно параллельно), которые можно запустить и забыть, а в конце забрать результат. Потоки сканирования директорий, судя по коду из вопроса, не зависят друг от друга, и мы наш ThreadManager можем разбить на две переменные типа ScanTask

    #include <iostream>
    #include <future>
    #include <atomic>
    #include <memory>
    #include <string>
    
    class ScanTask {
    public:
    	struct Result {
    		enum {
    			Success, Failure, Canceled
    		} status;
    		std::string message;
    	};
    
    	ScanTask(const std::string& dir)
    		: canceled_{ false },
    		result_{std::async(&ScanTask::scan, this, dir)}
    	{ }
    
    	void cancel() {
    		canceled_ = true;
    	}
    
    	Result result() { 
    		return result_.get(); // result_.valid() == false
    	}
    
    	Result scan(const std::string& dir) {
    		struct Finalize {
    			~Finalize() {
    				// emit signal scan completed
    			}
    		} finalizeSignal;
    
    		while (!canceled_ /* && not done */) {
    			// .. scan subdirectories
    			/*
    			if (something failed)
    				return {Result::Failure, "Failure"};
    			*/
    		}
    		if (canceled_)
    			return { Result::Canceled, std::string("[Canceled] ") + dir };
    		return { Result::Success, std::string("[Success] ") + dir };
    	}
    
    private:
    	std::atomic<bool> canceled_;
    	std::future<Result> result_;
    };
    
    int main() {
    	auto localScanTask = std::make_unique<ScanTask>("a");
    	localScanTask->cancel();
    	std::cout << localScanTask->result().message << std::endl;
    
    	auto sharedScanTask = std::make_unique<ScanTask>("b");
    	sharedScanTask->cancel();
    	std::cout << sharedScanTask->result().message << std::endl;
    	return 0;
    }

    Можно было даже не создавать отдельный класс, а в функции-обработчике нажатия кнопки запуска присваивать future члену виджета результат вызова async от анонимной лямбды, как предлагали с thread в ревью. При этом atomic canceled_ можно захватить по ссылке, она была бы общей для всех.
    Ответ написан
    Комментировать
  • Как переделать код под ООП?

    @code_panik
    Сначала замечу, что нет никакого смысла переписывать код в стиле ООП, потому что класс, назовем его Calculator, не имеет состояния, не хранит данных, состоит только из функций. То есть весь код будет выглядеть как раньше, но придется приписывать в начало операций имя объекта класса типа Calculator, напр. calculator.divide(1, 2);. Достаточно собрать эти функции в нашем пространстве имен my_math, например,
    namespace my_math {
    bool operator<(const std::string& st1, const std::string& st2) {
        size_t len1 = st1.length();
        size_t len2 = st2.length();
        if (len1 != len2) return (len1 < len2);
        long int i = 0;
            // ищем разряд, в котором значения отличаются
        while (i < len1 && st1[i] == st2[i])
        {
            i++;
        }
            // если разряд найден, то меньше число с меньшей цифрой для положительных и с большей цифрой для отрицательных, иначе числа равны
        return (i < len1) && ((st1[i] < st2[i]) );
    }
    bool les(int lhs, int rhs) {
        return lhs <= rhs;
    }
    }
    
    int main() {
        using my_math::operator<; // иначе cout << my_math::operator<(s1, s2) << endl;
        std::string s1 = "1", s2 = "2";
        cout << (s1 < s2) << endl;
    
        int a = 1, b = 2;
        cout << (my_math::les(a, b)) << endl;
        return 0;
    }

    Для ООП можно воспользоваться примером https://godbolt.org/z/rEPzdKe4j. В нем определены два файла: calculator.h и calculator.cpp с объявлением класса и реализацией соответственно.
    Замечу, что в реализациях бинарных операций не следует передавать string по значению, чтобы избежать лишних копирований. Если заменить тип аргумента std::string на ссылочный const std::string&, придется поправить код, чтобы он не изменял значения аргументов.
    Ответ написан
    Комментировать
  • Что за скаляр получается при скалярном произведении векторов?

    @code_panik
    Короткий и внятный ответ сложно дать, потому что скалярное произведение вводится разными способами. Почитайте учебники. Самый простой, по-моему, Ильин, Позняк - "Аналитическая геометрия". Здесь формула для координат определяется через ("геометрическую") формулу произведения длин на косинус. В "Введение в алгебру, Ч. 2 Линейная алгебра" Кострикина через длину вектора как квадратичную форму (и формулу для координат как соответствующую билинейную форму) и формулу косинусов (из школьного курса) получается "геометрическая" формула. В общем случае скалярное произведение вводится аксиоматически через основные свойства, напр. в Александров - "Курс аналитической геометрии" (также см. определение в конце ответа).
    На всё это можно смотреть как на описание ещё одного замечательного объекта из мира математики, который мы только открываем, а не выдумываем произвольным образом.
    Интуитивно от себя, забудем о формализме и пофантазируем. Воспользуемся геометрическим определением вектора. У вектора есть направление (знаем углы между векторами) и длина. Пусть мы хотим вектора умножать как-нибудь, и пусть, как вариант, результат умножения - число (если результат - вектор, будем фантазировать как-нибудь по-другому). Обозначим умножение a на b через (a, b). Пусть умножение обладает свойствами коммутативности (или симметричности (a, b) = (b, a)) и линейности ((k * a + r * b, c) = k * (a,c) + r * (b, c), k, r - числа). Вместе с симметричностью и линейностью у нас есть билинейность - линейность по каждому аргументу.
    Билинейность позволяет раскрыть наше произведение через произведение базисных векторов. Выберем стандартный базис из взаимно перпендикулярных векторов единичной длины. Обозначим их e1, e2 в двумерном случае. Тогда (a, b) = a1 * b1 * (e1, e1) + a1 * b2 * (e1, e2) + a2 * b1 * (e2, e1) + a2 * b2 * (e2, e2), в частности (a, a) = a1^2 * (e1, e1) + 2 * a1 * a2 * (e1, e2) + a2^2 * (e2, e2). Наше определение произведения станет согласованным с привычным определением длины |a| вектора через проекции по теореме Пифагора, если договоримся, что произведение является положительно определенным, то есть всегда (x, x) >= 0, и (x, x) = 0 только для нулевого x. Также пусть (ei, ej) = 1 только когда i = j, иначе 0. Получим (a, a) = a1^2 + a2^2 = |a|^2. Такое хорошее и удобное наше произведение и есть скалярное произведение - положительно определенная симметричная билинейная функция (форма). Формулу с косинусом можно вывести из этого определения с помощью школьной теоремы косинусов (см. Кострикин).
    Работа силы вычисляется как произведение величины перемещения на проекцию силы на вектор перемещения. Так совпало, что "геометрическая" формула скалярного произведения подходит для вычисления работы, и, значит, подходит формула в координатах выше.
    Ответ написан
  • Как получается формула N*(N-1) / 2?

    @code_panik
    Пусть мы сортируем пузырьком последовательность из N элементов N, N - 1, N - 2, ..., 1 по возрастанию. Тогда число N сделает N - 1 шагов, пока не окажется на своем месте на последней позиции. Аналогично число N - 1 сделает N - 2 шага и т.д. Когда мы разместим на свои места все элементы 2, 3, ... N, единичка автоматически окажется на своем месте, другого ведь нет. Получим, что в худшем случае общее число шагов сортировки последовательности из N элементов - это сумма N - 1 слагаемых (N - 1) + (N - 2) + ... + 1 = N * (N - 1) / 2 = | по формуле арифметической прогрессии | = (N - 1 + 1) * (N - 1) / 2.
    Ответ написан
    Комментировать
  • C++ | Почему новый поток со временем периодически перестаёт работать?

    @code_panik
    Наиболее вероятная проблема - race condition (состояние гонки) + ub (неопределенное поведение) из-за проблем синхронизации основного потока, в котором вызывается деструктор ~MyClass, и потока thr_dPManager.
    Пусть в основном потоке вызывается деструктор, но exit_flag_dPManager = false, тогда this->thr_dPManager.join(); не выполнится (race condition) и без ожидания завершения run() в основном потоке будут тихо освобождены все данные связанные с объектом класса и объектом потока thr_dPManager. В run() мы попытаемся обратиться к освобожденным данным класса (ub) и получим непредсказуемое поведение программы. В c++ для каждого потока мы должны корректно вызывать join или detach. Вынесем join из if,
    MyClass::~MyClass()
    {
        if (this->exit_flag_dPManager)
            this->exit_flag_dPManager = false;
    
        if (thr_dPManager.joinable())
            thr_dPManager.join();
    }

    Осталось проверить логику программы и убедиться, что "бесконечный" цикл завершится. Пока непонятно, зачем в деструкторе строка this->exit_flag_dPManager = false, ведь после завершения деструктора у нас не будет доступа к данным класса.
    Если exit_flag_dPManager используется для синхронизации: завершить run, если завершается основной поток, — тогда можно писать
    MyClass() : exit_flag_dPManager(false) ....
    
    void run() {
      while (!exit_flag_dPManager.load()) { ... }
    }
    
    ~MyClass() {
      this->exit_flag_dPManager = true;
    
      if (thr_dPManager.joinable())
        thr_dPManager.join();
    }


    Также нужно иметь в виду потенциальные проблемы из статьи https://habr.com/ru/articles/444464/.
    Ответ написан
    Комментировать
  • C++. Почему в консоли выводит непонятные символы, хотя setlocale(LC_ALL, &amp;quot;RU&amp;quot;) прописан?

    @code_panik
    Если кратко, то у windows беда с многообразием представлений символов (кодировок) в терминале. Есть однобайтовые 866 (стандартная терминала), 1251 (ansi), есть многобайтовая 65001 (utf-8). Чтобы проверить кодировку, в терминале наберите chcp. Если нет особых требований к работе с кодировками, достаточно обычного подхода
    #include <iostream>
    #include <string>
    using namespace std;
    
    int main()
    {
      string words;
      cin >> words;
      cout << words;
      return 0;
    }

    Пример работает с 866 и 1251.
    Если нужна кодировка 1251 (и она не выбрана для терминала по умолчанию при запуске), запускайте программу из подготовленного окна терминала. Для этого в терминале наберите сначала однажды chcp 1251. Кодировка может понадобиться, если в исходном коде программы в кодировке 1251 будут строки с кириллицей.
    Ответ написан
    Комментировать
  • Какие виды функций есть в C++, и как их различать?

    @code_panik
    В c++ есть бинарный оператор <<, который для целых чисел является оператором сдвига битов числа влево на заданное количество позиций (то же, что и повторное умножение на два). Например, программа напечатает 2 << 1 == 4,
    #include <iostream>
    using namespace std;
    
    int main() {
        cout << "2 << 1 == " << (0b010 << 1); // 0b100
        return 0;
    }

    Одна из основных возможностей языка - перегрузка функций, в частности встроенных операторов (не всех). То есть мы можем определить свой тип данных со своей реализацией оператора <<.
    #include <iostream>
    using namespace std;
    
    struct Foo {
        Foo& operator<<(int x) {
            cout << "Integer: " << x << '\n';
            return *this;
        }
    
        Foo& operator<<(const char* str) {
            cout << "String: " << str << '\n';
            return *this;
        }
    };
    
    int main() {
        Foo foo;
        foo << 3; // Integer: 3
        foo.operator<<("Hello world"); // String: Hello world
        return 0;
    }

    Для компилятора запись foo << 3; значит именно вызов функции foo.operator<<(3). Аналогично в случае cout, объекта типа std::ostream, который представляет поток вывода (character output).
    https://en.cppreference.com/w/cpp/io/basic_ostream...
    Ответ написан
    Комментировать
  • Почему НЕ vim? Vim для серьезной разработки?

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

    В самом простом текстовом редакторе нам доступны функции редактирования последовательности символов.

    vim добавляет новые уровни абстракций, например команды и регулярные выражения. Поток символов получает структуру слов, строк, абзацев, содержимого (например в скобках) и т.п.

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

    Голый vim не заменит IDE, а IDE не заменит vim, потому что они существуют на разных уровнях абстракций.
    Можно в vim тащить функции IDE или в IDE тащить vim. Я выбрал второй путь и тащу голый vim с минимальным vimrc почти в каждую IDE. В visual studio, vs code есть vim plugin, в Idea есть свой плагин, наверное лучший из всех. В Qt Creator есть fake vim. При работе со всеми плагинами сталкивался с ограничениями функциональности самого vim, а fake vim был самым проблемным. Но если нет особых требований к работе в vim, этот путь намного проще. Поэтому лучше начинать с него, и только потом, если будет не хватать функций vim, переходить на vim с IDE плагинами.
    Ответ написан
    1 комментарий
  • Как в игровых движках реализованы отскоки?

    @code_panik
    Какие способы реализации взаимодействия двух тел реально используются в игровых движках?

    Отскок - один из вариантов разрешения коллизий. Тема очень большая, ответы можно найти по запросам collision detection, collision resolution, collision response. Примеры материала:
    https://blog.winter.dev/2020/designing-a-physics-e...
    https://realtimecollisiondetection.net/books/rtcd/
    Интересно, что каждое столкновение генерирует сигнал-событие, который должен быть обработан движком. Представьте, что у вас множество шариков падают на пол в ограниченном стенами пространстве. Шарики ударяются друг об друга, теряя энергию. Расстояния между ними становятся всё меньше, и в конечном итоге движок регистрирует такое количество событий, что всё виснет, и сам движок занимается только обработкой коллизий. Такое поведение нужно иметь в виду и обрабатывать соответствующим образом. То есть разрешение коллизий зависит от желаемых результата и точности симуляции. Например, в sokoban или тетрисе достаточно останавливаться в момент регистрации коллизии, в примере с шарами у нас continuous collision.
    Примеры того, как сделано в реальных движках можно найти в выступлениях разработчиков на gdc, напр. https://www.gdcvault.com/play/1017644/Physics-for-...
    Ответ написан
  • Как в c++ сохранить txt файл в кодировке ansi?

    @code_panik
    Под ANSI обычно подразумевается однобайтовая кодировка Windows-1251.
    Синонимы: CP1251; ANSI (только в русскоязычной ОС Windows).

    Символ типа char в C++ всегда имеет размер 1 байт, поэтому работаем с файлом самым обычным образом. Прочитанное содержимое (возможно части) файла можно сохранить в буфере типа std::string или массиве char. Файл в программе можно представить объектом потока: std::ifstream, std::ofstream, std::fstream. Выходной файл, в который запишем содержимое полученного таким образом буфера, будет иметь ту же кодировку ANSI.

    Проще всего прочитать всё содержимое файла в буфер и потом перезаписать файл полностью, например так.
    #include <fstream>
    #include <string>
    
     int main() {
    	 const std::string path = "<путь к файлу>";
    	 std::fstream file(path, std::ios::in);
    	 std::string text;
    	 std::getline(file, text, '\0');
    	 file.close();
    
    	 text += "[edit]\n";
    
    	 file.open(path, std::ios::out);
    	 file << text;
    	 return 0;
     }
    Ответ написан
    Комментировать
  • Как сделать коллизии для вращающейся фигуры SFML?

    @code_panik
    Upd: sfml не предоставляет средств для работы с физикой. Разрешение коллизий придется реализовать самому, пример.

    Нужно начать с того, что разрешение коллизий выполняется не для спрайта, который существует только для отображения объекта, а для связанной с объектом формы (множеством форм) - коллайдера (collider) (прямоугольника, окружности, параллелепипеда, сферы и т.п.). Коллайдер представляет собой упрощенную форму игрового объекта. Работа с такими формами значительно упрощает разрешение коллизий.

    В двумерном платформере коллайдером может быть прямоугольник. Если объект имеет квадратную форму, и мы можем пожертвовать столкновением с углами, то коллайдером можно выбрать аккуратно подогнанную по размеру окружность. Распространенный вид коллайдера - AABB (axis aligned bounding box), прямоугольник, стороны которого всегда параллельны осям координат. При повороте игрового объекта достаточно изменить размеры такого прямоугольника, чтобы объект оставался в его границах.

    Следующая за определением коллизий большая проблема - реализация реакции на коллизию. Если просто обнулить скорость в момент коллизии, объекты слипнутся, создавая бесконечную коллизию. Также нужно помнить о возможных длительных интервалах между полными итерациями игрового цикла, которые влияют на интервалы разрешения коллизий, если оно реализовано в потоке с игровой логикой или отрисовкой кадра, а не в отдельном потоке с постоянной частотой. Если интервал достаточно долгий, объекты могут проваливаться друг через друга, игрок может вылететь за рамки сцены.

    Если нужно реализовать именно игру, то посмотрите в сторону библиотек с готовыми реализациями. Если проект учебный, выберите простую форму коллайдера, например aabb или окружность, и поищите для них примеры разрешения коллизий. Примеров множество.
    Ответ написан
  • Как реализовать управление для двух игроков на одной клавиатуре SFML?

    @code_panik
    В самом простом случае можно реализовать так же, как реализовано управление одним игроком. Достаточно регистрировать события нажатия/отпускания клавиш. В sfml функция sf::Window::setKeyRepeatEnabled с аргументом false включает режим однократной регистрации нажатия клавиши. То есть событие keyPressed будет зарегистрировано однажды при нажатии клавиши, даже если она остается нажатой.

    Можем считать, что между событиями keyPressed и keyReleased клавиша остается нажатой. Сохраним нажатые клавиши с списке и на каждом шаге игрового цикла будем менять соответственно нажатым клавишам состояние игры, в том числе игрока.

    Пример реализации (C++17):
    #include <SFML/Graphics.hpp>
    #include <unordered_set>
    #include <unordered_map>
    
    using namespace std;
    
    void normalize(sf::Vector2i& v) {
    	int length = sqrt(1ll * v.x * v.x + 1ll * v.y * v.y);
    	if (length != 0) {
    		v.x /= length;
    		v.y /= length;
    	}
    }
    
    class Input {
    public:
    	using Keys = std::unordered_set<sf::Keyboard::Key>;
    
    	void registerKeyEvent(sf::Event e) {
    		if (e.type == sf::Event::KeyPressed)
    			pressedKeys_.insert(e.key.code);
    		else if (e.type == sf::Event::KeyReleased)
    			pressedKeys_.erase(e.key.code);
    	}
    
    	const Keys& pressedKeys() const {
    		return pressedKeys_;
    	}
    
    private:
    	Keys pressedKeys_;
    };
    
    class Player {
    public:
    	enum class Control {
    		moveUp, moveLeft, moveRight, moveDown
    	};
    
    	Player(sf::Vector2i initialPosition, sf::Color color)
    		: position_{ initialPosition } {
    		sprite_.setPosition(sf::Vector2f(initialPosition.x, initialPosition.y));
    		sprite_.setFillColor(color);
    		sprite_.setRadius(50);
    	}
    
    	Player& map(Control control, sf::Keyboard::Key key) {
    		keyControls_[key] = control;
    		return *this;
    	}
    
    	void process(const Input& input) {
    		velocity_ = {};
    		for (sf::Keyboard::Key k : input.pressedKeys()) {
    			if (auto it = keyControls_.find(k); it != keyControls_.end()) {
    				switch (it->second) {
    				case Control::moveDown:
    					velocity_.y = 1;
    					break;
    				case Control::moveUp:
    					velocity_.y = -1;
    					break;
    				case Control::moveLeft:
    					velocity_.x = -1;
    					break;
    				case Control::moveRight:
    					velocity_.x = 1;
    					break;
    				default:
    					break;
    				}
    			}
    		}
    		normalize(velocity_);
    		velocity_ *= speed_;
    	}
    
    	void move(sf::Vector2i position) {
    		position_ = position;
    		sprite_.setPosition(sf::Vector2f(position.x, position.y));
    	}
    
    	const sf::CircleShape& sprite() const {
    		return sprite_;
    	}
    
    	sf::Vector2i position() const {
    		return position_;
    	}
    
    	sf::Vector2i velocity() const {
    		return velocity_;
    	}
    
    private:
    	int speed_ = 300; // pixels / s
    	sf::Vector2i velocity_;
    	sf::Vector2i position_;
    	
    	sf::CircleShape sprite_;
    
    	std::unordered_map<sf::Keyboard::Key, Control> keyControls_;
    };
    
    class Game {
    public:
    	Game(sf::RenderWindow* gameWindow)
    		: window_(gameWindow) {
    		window_->setKeyRepeatEnabled(false);
    		auto width = window_->getSize().x;
    		auto height = window_->getSize().y;
    		Player mrRed(sf::Vector2i(width / 3, height / 3 * 2), sf::Color::Red);
    		mrRed.map(Player::Control::moveUp, sf::Keyboard::W)
    			.map(Player::Control::moveDown, sf::Keyboard::S)
    			.map(Player::Control::moveLeft, sf::Keyboard::A)
    			.map(Player::Control::moveRight, sf::Keyboard::D);
    		players_.emplace_back(std::move(mrRed));
    
    		Player mrBlue(sf::Vector2i(width / 3 * 2, height / 3 * 2), sf::Color::Blue);
    		mrBlue.map(Player::Control::moveUp, sf::Keyboard::Up)
    			.map(Player::Control::moveDown, sf::Keyboard::Down)
    			.map(Player::Control::moveLeft, sf::Keyboard::Left)
    			.map(Player::Control::moveRight, sf::Keyboard::Right);
    		players_.emplace_back(std::move(mrBlue));
    
    		Player mrGreen(sf::Vector2i(width / 2, height / 3), sf::Color::Green);
    		mrGreen.map(Player::Control::moveUp, sf::Keyboard::I)
    			.map(Player::Control::moveDown, sf::Keyboard::K)
    			.map(Player::Control::moveLeft, sf::Keyboard::J)
    			.map(Player::Control::moveRight, sf::Keyboard::L);
    		players_.emplace_back(std::move(mrGreen));
    	}
    
    	void update() {
    		processInputEvents();
    		sf::Time dt = clock_.restart();
    		processMovement(dt.asMilliseconds());
    		drawScene();
    	}
    
    private:
    	void processInputEvents() {
    		for (sf::Event e; window_->pollEvent(e); ) {
    			if (e.type == sf::Event::Closed) {
    				window_->close();
    				return;
    			}
    			input_.registerKeyEvent(e);
    		}
    		for (auto& player : players_)
    			player.process(input_);
    	}
    
    	void processMovement(sf::Int32 dt) {
    		for (int id = 0; id < players_.size(); ++id) {
    			auto& player = players_[id];
    			sf::Vector2i position = player.position() + sf::Vector2i(
    				player.velocity().x * 1e-3f * dt,
    				player.velocity().y * 1e-3f * dt
    			);
    			player.move(position);
    		}
    	}
    
    	void drawScene() {
    		window_->clear(sf::Color::White);
    		for (const auto& player : players_)
    			window_->draw(player.sprite());
    		window_->display();
    	}
    
    	std::vector<Player> players_;
    	Input input_;
    	sf::RenderWindow* window_;
    	sf::Clock clock_;
    };
    
    
    int main() {
    	sf::RenderWindow mainWindow(sf::VideoMode(600, 600), "Circles", sf::Style::Close);
    	mainWindow.setFramerateLimit(120);
    	Game game(&mainWindow);
    	while (mainWindow.isOpen())
    		game.update();
    	return 0;
    }


    Мы создаем три круга, которые могут двигаться независимо. Клавиши движений назначаются в конструкторе Game.

    FrameLimit ограничен, потому что скорость и другие результаты расчетов целочисленные и привязаны к пискелям. При слишком высокой частоте кадров изменения расстояний на каждом шаге симуляции будут незначительными, округлятся к 0. Лучше заменить целочисленную арифметику на float-арифметику, чтобы не было грубых ошибок округления как в normalize, и промежуточных преобразований sf::Vector.

    За поддержание актуальности списка нажатых клавиш отвечает class Input.
    Ответ написан
    6 комментариев
  • Почему автодополнение не предлагает мне поля структуры?

    @code_panik
    Это особенность работы IntelliSense в Visual Studio. Более того, если в коде
    template<typename T>
    struct Outer {
      struct Node {
        int data;
      };
      void foo() { node.data; }
      Node node;
    };

    навести курсор на data внутри функции foo, скорее всего, получим подсказку <unknown> Outer<T>::Node::data, где unknown - тип, который не удалось вывести.

    IntelliSense позволяет подставить в шаблонный параметр значение конкретного типа. Для этого значение опции Template IntelliSense: Tools > Options > C/C++ > Advanced > IntelliSense > Enable Template IntelliSense должно быть true. Демонстрация работы функции: 1, 2, 3.

    Для заданной подстановки IntelliSense сгенерирует у себя экземпляр шаблонного класса, и подсказки будут работать как в случае с не шаблонным.
    Ответ написан
  • Источник данных не найден и не указан драйвер. Как исправить?

    @code_panik
    Используйте подходящий драйвер для подключения к базе данных вместо QODBC. Для postgresql это QPSQL (список драйверов в qt 5.15).
    Note: You should use the native driver, if it is available, instead of the ODBC driver. ODBC support can be used as a fallback for compliant databases if no native driver is available.
    QODBC for Open Database Connectivity (ODBC)
    При подключении с помощью ODBC в setDatabaseName указывается не имя базы данных, а datasource name или данные подключения (пример по ссылке выше).
    When connecting to an ODBC datasource, you should pass the name of the ODBC datasource to the QSqlDatabase::setDatabaseName() function, rather than the actual database name.
    Ответ написан
    Комментировать
  • Можно ли выделить память определенного размера?

    @code_panik
    В C++ можно, как и в C, выделить себе участок памяти, в котором будут жить объекты (в широком смысле, в том числе встроенного типа, напр. int). Чтобы начать жизнь объекта, его нужно разместить в памяти с соответствующим типу выравниванием. Если выравнивание, равное степени двойки, будет меньше требуемого для типа, то приведение с помощью static_cast типа указателя в выделенной памяти (void*, char*, unsigned char*) к указателю на тип объекта создает неопределенное поведение. Значит, чтобы безопасно использовать объект, размещенный в выделенной памяти, нужно убедиться, что он размещен (с помощью placement new) по выровненному адресу.

    Пусть мы создаем основной буфер под объект вызовом new unsigned char[size]. Будет ли буфер иметь достаточное выравнивание, чтобы в начале его разместить BUFFER::a типа int? - Да, если мы зарезервируем сразу достаточно памяти,

    In addition, if the new-expression is used to allocate an array of char, unsigned char, or std::byte (since C++17), it may request additional memory from the allocation function if necessary to guarantee correct alignment of objects of all types no larger than the requested array size, if one is later placed into the allocated array.


    Выражения new, new[] автоматически определяют размер выделяемой памяти и передают его низкоуровневым функциям operator new, operator new[] соответственно. Память, выделенная new, освобождается вызовом delete, для new[] - delete[]. Поведение delete похоже на поведение new выше.

    Значит, у нас есть всё необходимое чтобы реализовать свой класс буфера. Например,
    #include <cstddef>
    #include <iostream>
    #include <cstring>
    
    class Buffer {
    public:
      using preamble_type = int;
      static constexpr std::size_t preamble_size = sizeof(preamble_type);
    
      Buffer(const preamble_type& value, std::size_t bufferSize) {
        buffer_ = new unsigned char[preamble_size + bufferSize];
        ::new(buffer_) preamble_type(value);
      }
    
      Buffer(const Buffer& other) = delete;
      Buffer& operator=(const Buffer& rhs) = delete;
    
      Buffer(Buffer&& other) noexcept
        : buffer_(other.buffer_) {
          other.buffer_ = nullptr;
        }
    
      Buffer& operator=(Buffer&& rhs) noexcept {
        std::swap(buffer_, rhs.buffer_);
        return *this;
      }
    
      virtual ~Buffer() {
        destroy();
      }
    
      preamble_type& preamble() noexcept {
        return const_cast<preamble_type&>(
            const_cast<const Buffer *>(this)->preamble()
            );        
      }
    
      const preamble_type& preamble() const noexcept {
        return *preambleData();
      }
    
      unsigned char* data() const noexcept {
        return buffer_ + preamble_size;
      }    
    
      void resize(std::size_t size) {        
        if (buffer_ != nullptr) {
          auto newBuffer = new unsigned char[preamble_size + size];
          ::new(newBuffer) preamble_type(std::move(preamble()));
          destroy();
          buffer_ = newBuffer;
        }
      }
    
    private:
      preamble_type* preambleData() const noexcept {
        // return std::launder(reinterpret_cast<preamble_type*>(buffer_)); c++17
        return reinterpret_cast<preamble_type*>(buffer_);
      }
    
      void destroy() noexcept {
        preambleData()->~preamble_type();
        delete[] buffer_;
      }
    
      unsigned char* buffer_ = nullptr;
    };
    
    int main()
    {
      using std::cout;
      using std::endl;
    
      const std::size_t bufferSize = 100;
      Buffer b(100, bufferSize);
      const char hello[] = "hello world!";
      memcpy(b.data(), hello, sizeof(hello));
    
      auto c = std::move(b);
      cout << c.preamble() << ' ' << c.data() << endl;
    
      b = Buffer(5, 20);
      memcpy(b.data(), hello, sizeof(hello));
      cout << b.preamble() << ' ' << b.data() << endl;
      return 0;
    }


    Из кода можно выкинуть всё, что касается preamble_type, если считать, что первое поле всегда будет int. С другой стороны, код можно сделать обобщенным с минимальными изменениями. Например,
    template<typename PreambleT>
    class Buffer {
    public:
       using preamble_type = PreambleT;
       //...
    };


    Такой пример class Buffer, по-моему, скорее плохой, потому что только имитирует на C++ код, написанный на C. Так, мы не храним в классе размер буфера, поэтому нельзя определить операторы копирования. Детали реализации класса "утекают" в пользовательский код.
    Пока мы явно не сформулировали и не поддерживаем инварианты класса. Какое состояние объекта мы можем считать пригодным для использования?
    В частности, мы можем переместить содержимое буфера в другой объект конструктором перемещения, но потом к исходному буферу не можем применить resize. Даже если бы resize давал нам новый буфер, значение preamble потеряно. Если бы мы сохраняли preamble, тогда в конструкторе перемещения пришлось бы выделять память под новый буфер, но тогда этот конструктор уже не будет noexcept - плохо. Придется запретить перемещение?

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

    Наверняка есть лучшее проектное решение, такое которое не будет имитировать реализацию на C. Возможно, имеет смысл портировать решение на уровне интерфейсов, переписывая полностью на C++ детали реализации.
    Ответ написан
    Комментировать
  • Каковы правила конвертации указателя на массив неопределенной длины в указатель на массив определенной длины?

    @code_panik
    В выражении p = p2 мы выполняем неявное преобразование встроенных типов, для которых в стандарте не описаны правила преобразования. То есть такое преобразование не гарантировано, но может быть реализовано компилятором, например
    #include <type_traits>
    using namespace std;
    
    int main() {
        static_assert(std::is_convertible<int(*)[2], int(*)[]>::value, "Non-convertible");
        return 0;
    }

    компилируется в gcc 12.2, в clang 15.0 - ошибка.

    Существует старое не принятое предложение о закреплении такого преобразования стандартом.
    Ответ написан
    1 комментарий
  • Можно ли создать пустую структуру, а потом её заполнить внутри функции?

    @code_panik
    Можно ли создать пустую структуру, а потом её заполнить внутри функции?

    Нет, в языке c++ все типы данных должны быть известны компилятору при компиляции программы.

    Для работы с текстовыми настройками программы есть готовые решения, например qsettings в qt, libconfig.

    Если нужна своя реализация, можно придумать разные варианты интерфейсов для class Config. Если нам нужны отдельные объекты вроде struct Date, можем реализовать фабричный метод, который будет их создавать, как в следующем примере.

    #include <iostream>
    #include <string>
    #include <unordered_map>
    #include <fstream>
    
    class AppConfig {
    public:
        static AppConfig make(const std::string& path) {
    		    auto attributes = readAttributes(path);
    		    if (attributes.empty()) {
    			    // signal error - no attributes, no config
    			    return {};
    		    }
            return parse(attributes);
        }    
    
        std::string title = "";
        bool fullscreen = false;
        bool windowMode = false;
        int width = 0;
        int height = 0;
        int renderApi = 0;
    
    private:    
        static std::unordered_map<std::string, std::string>
        readAttributes(const std::string& path) {
            std::unordered_map<std::string, std::string> attributes;
            std::ifstream configFile(path);
            if (!configFile) {
                // signal error - failed to open file
                return {};
            }
            std::string attribute, value;
            while (configFile >> attribute >> value) // attribute value
                attributes[attribute] = value;
            return attributes;
        }
    
        static AppConfig parse(std::unordered_map<std::string, std::string>& attributes) {
            static constexpr auto trueValue = "true";
            AppConfig config;
            config.title = attributes["title"];
            config.fullscreen = attributes["fullscreen"] == trueValue;
            config.windowMode = attributes["window_mode"] == trueValue;
            config.width = std::stoi(attributes["width"]);
            config.height = std::stoi(attributes["height"]);
            config.renderApi = std::stoi(attributes["render_API"]);
            return config;
        }    
    };
    
    class Window {
    public:
    	static bool create_window(...) { return true; }
    };
    
    bool initialize() {
        auto config = AppConfig::make("settings.txt");
        if (!Window::create_window(config.title, config.fullscreen, config.windowMode, 
            config.width, config.height))
            return false;
        //...
        return true;
    }


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

    #include <unordered_map>
    #include <string>
    #include <any>
    
    class AppConfig {
    public:
        explicit AppConfig(const std::string& path) {  /* read values */  }
        std::any operator[](const std::string& attribute) {  return attributes[attribute];  }
    private:    
        std::unordered_map<std::string, std::any> attributes;
    };
    
    class Window {
    public:
      static bool create_window(...) { return true; }
    };
    
    bool initialize() {
        auto config = AppConfig("settings.txt");
        if (!Window::create_window(
            std::any_cast<std::string>(config["title"]),
            std::any_cast<bool>(config["fullscreen"]),
            std::any_cast<bool>(config["window_mode"])
            /* ... */)) {
            return false;
        }
        return true;
    }
    Ответ написан
  • Разобраться с моим кодом, можно?

    @code_panik
    Ошибка в попытке применить вызов функции к переменной типа int, а не функции.
    Возникает, когда в bot.register_next_step_handler(message, x1) передается не функция x1, а значение типа int, которое записывается в global x1 после первого запуска функции x1.
    Начните с переименования функций или переменных.
    Ответ написан
    5 комментариев
  • Является ли такой способ выделения массива объектов на хипе идиоматичным?

    @code_panik
    "Если требуется работа с голой памятью", - в c++ обычно не работают с голой памятью (free store). Работают с объектами, у которых есть время жизни. Мы выделяем память посредством низкоуровневых функций, только чтобы потом в ней создавать живые объекты, например с помощью placement new.

    В этом смысле подход "без использования векторов и прочего" не имеет совершенно никакого значения. Как руками мы выделяем память и создаем в ней объекты, так поступает и вектор. Но только вектор позволяет нам сохранить здоровыми несколько клеток головного мозга. Другие аргументы в соответствующем разделе super-faq по ключевому слову vector.

    "например, какое-то апи требует тип T *" - vector<T>::data().

    Идиоматичность кода подразумевает использование актуальных стандартных средств языка. Оба примера напоминают c++03. Если код плохо читаем как в первом примере, то имеет смысл поискать лучшие альтернативы. Если их нет, то завернуть код во что-нибудь удобочитаемое, вектор например. Про идиоматичную низкоуровневую работу с памятью можно почитать у Страуструпа в примере реализации вектора.

    Вопрос оптимизации - отдельная тема. Оптимизация под конкретные нужды не обязана быть идиоматичной или в принципе соответствовать общим представлениям. Например, вместо хранения массива структур, мы можем создать и использовать отдельно массивы полей структур. Можете поискать информацию по data-oriented design.
    Ответ написан
    Комментировать
  • Как превратить void() в void (**)()?

    @code_panik
    UPD: Похоже, проблема в библиотеке
    https://github.com/espressif/arduino-esp32/issues/7675
    Исправляющий комит
    https://github.com/espressif/arduino-esp32/commit/...

    Довольно странное требование для api.
    Если вы пишите класс-обертку над этой функцией, то она принимает просто функцию, а не указатель на указатель.
    В C++ функция определяется своим адресом, и правила передачи функции в функцию похожи на правила передачи обычного массива.
    Похоже, проблема именно в объявлении вашего register_callback, которое можно реализовать как в этом примере
    #include <iostream>
    using namespace std;
    
    using esp_spp_cb_event_t = int;
    using esp_spp_cb_param_t = void;
    
    void btCallback(esp_spp_cb_event_t event, esp_spp_cb_param_t *param) {
        cout << "btCallback" << endl;
    }
    
    class MyBt {        
    public:
        typedef void (*callback_type)(esp_spp_cb_event_t, esp_spp_cb_param_t *);    
    
        void register_callback(callback_type cb) {
            cb(0, nullptr);
        }
    };
    
    int main()
    {    
        MyBt bt;
        bt.register_callback(btCallback);
        return 0;
    }
    Ответ написан
    5 комментариев