Я всегда слышал, что с библиотеками в С++ что-то не так, как и с ограничением максимального целочисленного значения, да и вообще то, что язык сложный и непонятный. Что же, сегодня, мы начнём писать собственную библиотеку больших чисел, полностью своими руками c 0, и узнаем, так ли страшен С++, как его малюют?
Если вы не разбираетесь в С++, не переживайте, эта статья имеет нулевой порог вхождения. Мы начнём с лёгкого, но вы даже не заметите, как начнёте разбираться в более сложных и непонятных, на первый взгляд, вещах. Главное, писать код логично. Думаю, данная статья будет интересна не только начинающим, ведь я постарался затронуть достаточно много тем. (для старожилов: моя цель не сделать оптимизирование или быстрее, а показать, что С++ не такой уж и сложный язык программирования. И да, я знаю, что существуют другие библиотеки, которые делают это быстрее и лучше. И да, было бы круче, если бы мы использовали булевую алгебру. И да, С++ про вечную оптимизацию, но это статья не про это. Спасибо.)
За сегодня мы узнаем, что такое: Перегрузка функций/конструкторов, прототипы функций, обработка исключений, пространство имён, псевдонимы типов, заголовок.h, как пользоваться отладчиком и как писать продвинутые/красивые комментарии. Пристёгивайтесь, будет безумно интересно.
▍ Предисловие и планы
С++ строготипизированный язык программирования, где максимально возможное значение целочисленной переменной, является максимальное значение unsigned long long int (где-то 18 446 744 073 709 551 615). Этого бывает недостаточно, поэтому я решил разработать собственную библиотеку больших чисел (BigNumLib). Единственное ограничение размерности BigNumLib переменной – это количество цифр, из которого будет состоять число, то есть, максимально в число поместится 4 294 967 295 цифр.
▍ Начало разработки
Итак, начинаем разработку! Для начала нам необходимо продумать логику и возможности нашего собственного типа данных. Как мы создадим свой тип данных? В ЯП С++ нельзя расширить стандартные типы (int, double и т.п.), поэтому, единственный возможный вариант, который у нас остался, это работа через struct и class.
Чем отличаются Struct и Class?
Ответ: единственное различие между ними, так это то, что в struct модификаторы доступа по умолчанию public, а в class — private. Также отличается и наследование по умолчанию.
Итак, откроем Visual Studio с пустым проектом. Создадим папки (если они не созданы): “Файлы заголовков” с файлом BigNumLib.h и “Исходные файлы” с файлами Main.cpp, BigNumLib.cpp. У кого трудности на данном этапе, ничего страшного, ниже представлены фото и gif.
В файле заголовка (.h), можно заметить строчку #pragma once. Что это?
Ответ: В языках программирования С и C++ #pragma once — нестандартная, но широко распространённая препроцессорная директива, разработанная для контроля за тем, чтобы конкретный исходный файл при компиляции подключался строго один раз.
Здесь, в этом файле заголовка, мы будем создавать прототипы функций (функции без тела) и вообще описывать весь класс, например, какие дополнительные библиотеки будут подключены, для исправной работы нашей, или какие функции будут доступны пользователю, а какие нет (модификаторы доступа public, private). Зачем мы вообще создали данный заголовок? Всё просто, чтобы подключить внешний код, необходимо использовать именно заголовок.
▍ Создание bignum класса
Итак, для начала нам необходимо создать класс и его поля:
class bignum {
private:
std::string _value;
size_t _size;
bool _isNegative;
public:
bignum();
}
Ловим ошибку, что не подключили библиотеку и подключаем:
#include <string>
Итак, что мы написали?
_value =
здесь будет храниться наше число в виде строки_size =
из скольких цифр состоит наше число (размер). size_t
это псевдоним, то есть, то же самое что и unsigned int
(положительные целые числа)_isNegative =
является ли отрицательным числом. (true или false)bignum() =
конструктор класса. Он вызывается при создании экземпляра класса.private: –
поле, где доступ к данным имеет лишь класс. Приватные переменные, как правило, пишутся через ‘_’.Так отлично, теперь откроем BigNumLib.cpp и напишем там такой код:
#include "bignum.h"
bignum::bignum()
{
_value = "0";
_size = 1;
_isNegative = false;
}
Здесь мы подключили наш заголовок и описали конструктор класса, где доступ к конструктору мы получаем через пространство имён (
bignum::
)Теперь мы можем открыть наш основной файл (Main.cpp) и проверить работу библиотеки:
#include "BigNumLib.h"
int main() {
bignum a;
return 0;
}
// Важное замечание, локальные заголовки, которые находятся в одном решении, подключаются с помощью кавычек.Что же, теперь ставим точку остановы на
return 0
, и смотрим нашу переменную.Отлично, всё работает! По поводу отладчика, это безумно удобный интерфейс. Как сказал один мудрый человек, если программист не умеет пользоваться отладчиком, то этот человек не программист. Краткий экскурс по данному чуду: f5 (запуск отладки), shift+f5 (остановка), ctrl+shift+f5 (перезапуск), f10 (шаг с заходом в функцию), f11 (шаг с обходом функции), shift+f11 (шаг с выходом из функции), f5 (во время отладки, перейти к следующей точке остановы).
▍ Геттеры
Так, теперь создадим функцию геттер, чтобы иметь возможность читать поле нашего класса. Для этого объявим эту функцию в BigNumLib.h (файл заголовок) в поле
public
://@return string
std::string getValue();
Мы написали комментарии в стиле DOC++. Этот тип комментариев понимает Visual Studio и красиво отображает нам. (ключевые слова пишутся через ‘@’:
@return, @param
)Теперь необходимо прописать логику данных функций в BigNumLib.cpp.
std::string bignum::getValue()
{
std::string _value = this->_value;
if (this->_isNegative)
_value.insert(0, "-");
return _value;
}
Чтобы обратиться к внутренним полям класса, мы используем указатель
this
, таким образом, пользователь имеет доступ к переменной _value
только на чтение.В функции
getValue()
мы создаём локальную переменную _value и заполняем её данными лежащими в поле класса. Если поле _isNegative имеет значение true (число отрицательно), то мы вставляем в начало строки ‘-’. (insert(позиция, знак))Отлично, теперь проверим наш код в действии!
#include "BigNumLib.h"
#include <iostream>
int main() {
bignum a;
std::cout << "bignum a = " << a.getValue() << std::endl;
return 0;
}
▍ Перегрузка конструктора (приём int)
Далее нам необходимо создать перегрузку конструктора класса, который на вход принимает long long int. Для этого объявим:
BigNumLib.h
public:
bignum(long long int other_value);
BigNumLib.cpp
bignum::bignum(long long other_value)
{
_isNegative = other_value < 0 ? true : false;
_value = _isNegative ? std::to_string(other_value).erase(0, 1) : std::to_string(other_value);
_size = _value.size();
}
Перегрузка конструктора \ функции \ оператора – это когда они имеют одно и то же имя, но принимают разные параметры. Та или иная функция вызывается в зависимости от принимаемых ею аргументов.
Здесь, мы впервые использовали тернарный оператор. Сокращённое написание конструкции
if, else
. Всё предельно просто, если на вход поступает отрицательное число, то поле _isNegative
становится true
. После чего, число переводится в строку и если число отрицательное, то удаляется первый символ из строки (‘-’).Сейчас мы можем протестировать это и присвоить число. Попробуем присвоить положительное и отрицательное число, посмотрим, правильно ли работает наша программа:
Отлично, теперь попробуем ввести огромное число:
Как мы можем видеть, Visual Studio запрещает нам присваивать такое огромное число, потому что оно выходит за рамки long long int (превысив значение самого большого стандартного типа). Как мы будем обходить данный запрет? С помощью строки, ведь она, практически безгранична.
▍ Перегрузка конструктора (приём string, char*)
Создадим конструктор и пару функций в файле заголовка.
BigNumLib.h
public:
bignum(const char* other_value);
bignum(std::string other_value);
private:
void parsStringToBigNumParams();
BigNumLib.cpp
bignum::bignum(const char* other_value)
{
_value = other_value;
parsStringToBigNumParams();
}
bignum::bignum(std::string other_value)
{
_value = other_value;
parsStringToBigNumParams();
}
void bignum::parsStringToBigNumParams()
{
if (_value[0] == '-') {
_value = _value.erase(0, 1);
_isNegative = true;
}
else
_isNegative = false;
if (_value.find_first_not_of("0123456789") != std::string::npos)
throw std::runtime_error(_value + " it's not a number!");
_size = _value.size();
}
Мы написали 2 конструктора, где один из них принимает string, а другой массив char. Зачем? Потому что в случае, когда после присвоения сразу записывается значение, то будет массив char. А если создать string переменную и присвоить уже её, то активируется другой конструктор (со string параметром).
По поводу функции
parsStringToBigNumParams()
. Данная функция превращает строку, в набор параметров нашего класса. В начале она проверяет, стоит ли ‘-’, на первой позиции в _value
, если да, убрать знак из строки и присвоить параметру _isNegative = true
. После чего идёт проверка, если в _value найдено, что какой-то элемент не совпадает с цифирным набором (npos — не найдено совпадений), то выкинуть исключение. И дальше присвоить размер.Что делает исключение? Полностью останавливает работу программы, если его не обработать. Давайте же опробуем работу наших конструкторов и обработаем исключение:
Как работает обработка исключений? В блоке
try
пишется небезопасный код, если в нём происходит ошибка, мы тут же попадаем в блок catch
. Из-за того, что мы добавили описание в нашу самодельную ошибку, мы можем увидеть, какое значение, смогло сломать нашу библиотеку.▍ Заключение
Что же, я чувствую, что выдал достаточно много инфы и, если продолжить, у новичков она может превратиться в кашу. За сегодня мы прошли очень много интересных тем и познакомились с некоторыми особенностями языка С++, но впереди ещё больше крутой информации, такая как перегрузка операторов, указатели, resize string и собственная логика в математических операциях. Если вам заходит такой формат обучения/разработки реального проекта, дайте знать, буду пилить 2 часть в таком же формате, ну, если вы вообще ждёте 2 часть :)
p/s Ссылка на GitHub
Комментарии (52)
sidorovmax
13.09.2021 16:57+13"
_isNegative = other_value < 0 ? true : false;
"
- это за пределами добра и зла.
За такое с технического собеседования сразу вылетают.lonely_programmer Автор
13.09.2021 17:13+1Моя задача была показать как можно больше вариантов написания кода. Описание логики через тернарный оператор является одним из таких способов. Данный пример показывает, что и в С++ можно такое реализовывать в одну строчку. +Если это не показывать, новички не будут знать о существовании таких методов.
Kotofay
13.09.2021 17:58А именовать локальную переменную начиная с "_" перед этим проговорив что с "_" начинают приватные члены класса?
Это намеренное введение в заблуждение.Не каждый начинающий не забудет ставить this-> перед членом класса и будет долго думать почему его переменная присваивается не туда.
lonely_programmer Автор
13.09.2021 18:03Пока что, проблем с this возникать не должно. Во второй части, когда мы будем иметь работу с другим классом (математические операции), данную тему я раскрою лучше.
TheRikipm
13.09.2021 19:09+9ИМХО, демонстрация какой-либо конструкции языка в месте где она не нужна, хуже чем отсутствие её демонстрации вообще. Новичок может решить что подобная запись является нормой и через неуоторое время ему будет сложно перестроиться.
Kiever1
14.09.2021 14:37А почему это плохо?
bogolt
14.09.2021 15:46+2other_value < 0
уже возвращает bool ( меньше или нет )
затем мы bool превращем в bool
если разбить это на 2 строки получится такbool isNeg = other_value < 0; return isNeg ? true : false; // если isNeg true вернуть true иначе вернуть false
чувствуете некую избыточность?Kiever1
14.09.2021 17:10Да, точно. Что-то несообразил.
Наверное меня сбил с толку win32 стиль, там бывают такие преобразования для BOOL.
thecove
14.09.2021 15:39-1и что плохого в этой записи?
ну лично мне режет глаз _isNegative
я бы так написал:
const bool isNegative = other_value < 0 ? true : false;
Naf2000
14.09.2021 19:24+1Тернарный оператор не нужен. Операция сравнения уже даёт bool
thecove
15.09.2021 07:12+1Не, ну тут то вы абсолютно правы.
const bool isNegative = other_value < 0;
Я подумал что сама конструкция sometype val = other_value < 0 ? X : Y; вызывает отторжение. И человек предпочитает if юзать.
isNegative = Y;
if( other_value < 0 ) isNegative = X;
sergegers
13.09.2021 17:00+15На всякий случай, если кто вдруг подумает, что так пишут на C++. Это не так. Никто не будет хранить число в строке и конвертировать его туда и обратно. Есть, например, библиотека Boost.Multiprecision и, уверен, ещё несколько библиотек, которые позволяют работать с большими интами без оверхеда.
lonely_programmer Автор
13.09.2021 17:21+2Похоже, вы невнимательно читали статью, если вообще читали. В самом начале, я написал, что библиотека реализована на string переменных и это не есть хорошо. Я написал, что гораздо лучше использовать вместо этого. Предупредил, что многое сделано для упрощения. Да и настоящие математические библиотеки всегда будут быстрее самодельных и я не стремился их обогнать. Моя задача предельно проста - начать с азов объяснять С++ и показывать разношёрстные примеры, как выполнить те или иные действия. Если вы считаете, что статья не подходит для начинающих, я готов вас выслушать.
Kotofay
13.09.2021 17:27+1Вы дважды упомянули public и private но ни слова не сказали о том, что стоит за этим, ни одного слова об инкапсуляции. А ведь это один из столпов ООП в С++.
lonely_programmer Автор
13.09.2021 17:35+2Я же описал для чего мы используем поля public и private. И немного затронул тему геттеров. getValue() - является одним из таких, где возвращает поле _value прямиком из private. Возможно, нужно было создать 2 класс и показать наследование, чтобы углубиться в ООП, но мне показалось, что это будет перебором.
Kotofay
13.09.2021 17:52+1Инкапсуляция к наследованию никак не относится. И 2-й класс действительно будет лишним.
Ограничение доступа к членам класса это побочный эффект, основное их предназначение -- сокрытие деталей реализации, или инкапсуляция.
Вы могли бы описать public члены класса как некий интерфейс к реализации. Тогда детали реализации становятся не важны для пользователя класса. И модифицировать реализацию можно сколь угодно часто, при этом программа использующая этот класс не меняется, и ничего не знает об глубинных изменениях в классе.
sergegers
13.09.2021 19:25+1Надо было написать вариант для чисел фиксированного размера на шаблонах, конечно. Волноваться не надо, у вас не получится невзначай математическая библиотека. Зато будет продемонстрирована идея, лежащая за такой библиотекой и показаны уникальные свойства C++. Если же вы не в силах сделать это, то разумнее было бы подобрать другой пример.
Ваше же решение написано на C с классами, может быть перенесено на массу языков типа Java, C#, а главное - идеологически неверно. Я бы сравнил его с поиском по отсортированному массиву последовательным перебором. Начинающих надо учить на упрощённых примерах, конечно, но не на неправильных.
INSTE
13.09.2021 17:31+1Кстати написать c++ binding к libtommath или openssl для bignum было бы куда поучительнее костыляния со строками.
lonely_programmer Автор
13.09.2021 17:50+1Что же, сегодня, мы начнём писать собственную библиотеку больших чисел, полностью своими руками c 0, и узнаем, так ли страшен С++, как его малюют?
Полностью своими руками с 0. Так что о подключении сторонних библиотек и мысли не было. Да и статью хотелось сделать не такой сложной. А можно ли было бы сделать оптимизирование? Да, конечно, я это говорил прямым текстом в начале статьи.
xi-tauw
13.09.2021 17:44+5За идею статьи ставлю плюс, а за реализацию - минус.
1) В std::string bignum::getValue() сразу стреляем себе в ногу создавая переменную _value перекрывающую поле класса.
2) Продолжаем рубрику вредные советы кидая исключение в конструкторе. Причем, с неинформативным сообщением. Вот что должен понять программист, увидев "-123 it's not a number"? Ведь, как раз -123 число.
3) Зачем перегружаем конструктор и для const char* и для std::string? Сами прямо задаете этот вопрос, но отвечаете мимо. Конструктора с std::string вполне хватило бы.
lonely_programmer Автор
13.09.2021 18:01+1Я не совсем понял про перекрытие поле класса. Нам же нельзя позволить пользователю изменять данный параметр (поле). Получить его, он может, изменить - нет. На самом деле, эта функция заглушка, в 1 части я просто не успел рассказать про перегрузку операторов потокового вводы/вывода.
В первой ревизии ошибки кидались через специальную функцию my_exeption и было дополнительное поле _isNum. При каждой математической операции данное поле проверялось на true. Спустя время было решено отказаться от данной реализации, по причине громоздкости кода. P.S. вывод ошибки с минусом и правда неверный, я забыл добавить if(_isNegative) добавить минус в начало строки.
-
Честное слово, у меня такой же вопрос к моей Visual Studio. Ей недостаточно одного string конструктора. На самом деле, гораздо лучше было бы использовать функцию шаблон, но это уже во 2 части.
Спасибо за отзыв!
xi-tauw
13.09.2021 19:02-
Вопрос в имени переменной. Вы дали одинаковые имена переменной во внутренней области видимости (функция) и внешней (класс). Написали бы проще.
Вам бы еще нормализацию прикрутить к вариантам типа "-00000".
-
Нет, это вопрос к вам.
UPD: третий вопрос снимается, я понял, что вы хотели сделать.
-
NeoCode
13.09.2021 18:27Это чисто учебный проект? Есть же GMP и Boost.Multiprecision который ее использует (хотя в бусте есть и собственная реализация).
А вообще конечно поддержка длинных чисел должна быть встроена в язык на уровне литералов.lonely_programmer Автор
13.09.2021 20:37Да, это полностью учебный проект. О неэффективности данного подхода было сказано в самом начале статьи.
static_cast
13.09.2021 20:11+1Для чисто учебного проекта норм. А вообще, хранить разряды в char-ах неэффективно, так только чуть удобнее для человеческого восприятия при отладке. Обычно переходят к системам счисления, основанным на длине машинного слова. Допустим, с основанием в 32 бита (половина от 64, чтобы было удобнее - избегать обработки переполнения при поразрядных операциях). Это быстрее и занимает меньше памяти.
lonely_programmer Автор
13.09.2021 20:40О неэффективности данного подхода (реализация через string) было сказано в самом начале статьи. Я слышал, что лучше всего использовать булевую алгебру. Ваш подход звучит очень интересно, я бы с удовольствием изучил его.
yeputons
13.09.2021 20:50Если основывать на системе счисления, не являющейся степенью десятки, то придётся переводить в десятичную систему и обратно. Наивный подход будет работать за квадратичное время, получим как в Python или Java: вычисления мгновенные, а вот вывод числа на миллион разрядов невозможен. Является ли это проблемой — зависит от задачи.
static_cast
13.09.2021 21:42Ну, машинная математика, основанная на степени двойки вас же устраивает. =)
yeputons
13.09.2021 21:52Устраивает, потому что вывод работает.
Если у меня задача, в которой надо зачем-то посчитать большое десятичное число (обычно в развлекательных/учебных целях), то перестаёт устраивать, потому что у меня перестаёт работать вывод ответа на экран. Например, запустите на питоне вот такой код:
x = 10 ** 1000000 + 4 print(x % 10) print(x)
Само число
x
из миллиона знаков посчиталось быстрее, чем за секунду, равно как и его остаток от деления на 10. А вот вывод этого числа целиком занимает некоторое время, потому что идёт перевод из двоичной системы в десятичную неэффективным способом.static_cast
13.09.2021 22:08+1Обычно все-таки вывод занимает незначительную долю в тех расчетах, для которых требуется bignum. И почему тормозит вывод еще нужно смотреть. Особенно в Питоне. Может быть, print зашился на аллокации памяти. Или тормозит какой-нибудь форматтер вывода для bignum. Или, банально, компилятор лениво вычислил x не в момент декларирования операций, а перед использованием, выполняя print(x % 10), а вы решили что это перевод в десятичную систему. Правильный тест зачастую сложнее алгоритма, который он тестирует.
yeputons
13.09.2021 22:29Обычно все-таки вывод занимает незначительную долю в тех расчетах, для которых требуется bignum.
Да, зависит от задачи, всё верно.
И почему тормозит вывод еще нужно смотреть
Он не просто тормозит, он у меня минуту работает. А вот если выводить как
print(bin(x))
— то пару секунд.Я не спец в CPython, но файл с названием
longintrepr
выглядит многообещающе: вот тут нам сообщают, что всё действительно хранится в двоичной системе счисления (точнее, с основанием 2**30), а вот тут парой вложенных циклов переводят число из одной системы счисления в другую перед выводом в строку. Даже на Кнута сослались.Вывод: вывод длинного числа в CPython работает за квадратичное от длины числа время. 10**12 операций — это не шутка, даже если соптимизировать в десяток-сотню раз.
winwood
14.09.2021 11:48во-первых, какой смысл в печати числа, занимающего несколько страниц? Человек все равно его воспринимает лишь на уровне "ух, какое большое".
во-вторых, библиотека GMP, которую уже упоминали в комментариях, доступна и в питоне. Добавление к Вашему кодуimport gmpy2
print(gmpy2.mpz(x))
творит чудеса - все преобразуется и печатается "мгновенно" )yeputons
14.09.2021 20:16во-первых, какой смысл в печати числа, занимающего несколько страниц? Человек все равно его воспринимает лишь на уровне "ух, какое большое".
Для развлечения или сверки реализации с какой-нибудь эталонной. Наверное, для принятия бизнес-решений действительно не нужно больше десятка-другого точных знаков, хотя я не специалист.
во-вторых, библиотека GMP, которую уже упоминали в комментариях, доступна и в питоне.
Так я же не против библиотек. Я против догмы "в двоичной системе всё точно будет быстрее" — нет, не всегда будет.
А в GMP просто используют более сложный алгоритм перевода между системами счисления с разделяй-и-властвуй и наверняка быстрым делением из того же GMP: https://github.com/alisw/GMP/blob/2bbd52703e5af82509773264bfbd20ff8464804f/mpn/generic/get_str.c#L307 . Я ожидаю O(n log^2 n) и ещё кучу неасимптотических оптимизаций сверху. Так что результат вполне ожидаемый.
VladBolotov
13.09.2021 20:41+8Как по мне, "библиотека для работы с большими числами" очень плохой пример для демонстрации возможностей С++. Вам осознано придется упускать кучу важных моментов/нюансов, приследую "обазовательные" цели.
Теперь по статье/коду:доступа по умолчанию public, а в class — private
При объявлении шаблонных параметров мы можем использовать class, а struct нет. Какой посыл то в этой информации?
#pragma once — нестандартная, но широко распространённая препроцессорная директива
Что значит нестандартная? Не описанная в ISO/IEC 14882? Что такое препроцессор? В чем разница между
#pragma once
и#ifndef #define #endif
? Статья же для "новичков", правда?Всё просто, чтобы подключить внешний код, необходимо использовать именно заголовок.
Нет.
Итак, для начала нам необходимо создать класс и его поля:
Большинство проектов на C++ используют последовательность из public/protected/private.
size_t _size;
Зачем нам хранить size, кода он есть в std::string? Еще одна возможность стрельнуть себе в ногу или создать неконсистентность?
Приватные переменные, как правило, пишутся через ‘_’.
Заклинаю вас, никогда так не делайте и не давайте таких советов.
https://eel.is/c++draft/lex.name#3.2Далее нам необходимо создать перегрузку конструктора класса
Кому необходимо? Почему необходимо? Почему именно
long long int
?Перегрузка конструктора \ функции \ оператора
Подводные камни? Неявные преобразования типов? AFAIK не во всех языках есть перегрузки операторов, конвертирующие конструкторы, конвертирующие операторы и т.д. Неплохо было бы хоть немного описать что это за зверь.
parsStringToBigNumParams()
Почему бы не показать как сделать сразу хороши и рассказать про delegating constructor?
Izaron
18.09.2021 20:58+2Заклинаю вас, никогда так не делайте и не давайте таких советов.
https://eel.is/c++draft/lex.name#3.2В данном случае нарушения нет, такие символы как члены класса, локальные переменные, методы и переменные внутри блока
namespace
- не являются находящимися в "global namespace".
yeputons
13.09.2021 21:05+6Мне кажется, в реализации много мест, которые можно и нужно сделать гораздо аккуратнее. Даже если считать, что хранение по десятичным разрядам — это окей с точки зрения быстродействия и поддержания инвариантов.
Из алгоритмического:
Убрать поле
_size
— либо оно всегда равно_value.size()
, либо это что-то непонятное. Незачем усложнять инвариант класса. Можно забыть случайно обновить, а так код короче станет.Строго описать допустимые комбинации значений для оставшихся полей. При наивной реализации очень легко огрести от ведущих нулей, знакового нуля, вычитания чисел похожей длины.
Из C++-специфичного:
Использовать member initialization list в конструкторе вместо переприсваивания полей.
Не начинать переменные с
_
— это допустимо для полей, но, например, для глобальных переменных — неопределённое поведение (undefined behavior, UB).Принимать строки по константным ссылкам, где можно. Или по значению и перемещать. В конструкторе, например, сейчас лишнее копирование.
Можно сделать user-defined literal.
Для тестов удобно взять какую-нибудь библиотеку юнит-тестов вроде onqtam doctest.
Дальше посмотрел на код вашей библиотеки на GitHub:
Вместо своих методов
swap
можно использовать стандартныйstd::swap
. К тому же их совершенно незачем делать методами, могли бы быть свободными функциями, причём видимыми только внутри.cpp
. А для этого их стоит заключить в unnamed namespace.Функцию сравнения чисел можно написать гораздо короче и проще, если использовать паттерн вроде
if (x != y) return x < y;
.Кажется, что есть много дублирования кода между разными функциями сравнения, сложения и вычитания. Концептуально случаев очень мало, но у вас, мне кажется, многовато получилось.
lonely_programmer Автор
13.09.2021 21:38Спасибо за такой информативный отзыв. На счёт _size переменой. Она и правда является лишней, спасибо всем, кто подметил это. В своё время, я создал её, потому что думал, что взять значение из _size гораздо выгоднее, чем высчитать str.size().
Насчет кода на GitHub: проводится масштабна оптимизация. Swap функцию я решил написать в виде typedef функции.
По поводу if(x!=y) return x<y. Красивое элегантное решение, но оно мне не подойдёт, так как приходится сравнивать числа поцфыорно (если можно так сказать).
Спасибо большое за отзыв!
yeputons
13.09.2021 21:54Красивое элегантное решение, но оно мне не подойдёт, так как приходится сравнивать числа поцфыорно (если можно так сказать).
Можно в цикле то же самое написать. Ну и раз уж у вас всё равно цифры хранятся как строчки — то можно сначала сравнить по длине, а потом просто как строчки при помощи
<
, он поддерживаетсяstd::string
. Правда, тут надо аккуратно с инвариантами.static_cast
13.09.2021 22:16Можно в цикле то же самое написать. Ну и раз уж у вас всё равно цифры хранятся как строчки — то можно сначала сравнить по длине, а потом просто как строчки при помощи
<
, он поддерживаетсяstd::string
. Правда, тут надо аккуратно с инвариантами.Можно сразу оператором. Не стоит недооценивать авторов имплементаций STL, сравнение размера там, конечно же, есть.
template <class _Traits> constexpr bool _Traits_equal(_In_reads_(_Left_size) const _Traits_ptr_t<_Traits> _Left, const size_t _Left_size, _In_reads_(_Right_size) const _Traits_ptr_t<_Traits> _Right, const size_t _Right_size) noexcept { // compare [_Left, _Left + _Left_size) to [_Right, _Right + _Right_size) for equality using _Traits return _Left_size == _Right_size && _Traits::compare(_Left, _Right, _Left_size) == 0; }
yeputons
13.09.2021 22:37+1Сравнение длин там нужно не для скорости, а для корректности: короткое число
99
меньше длинного123
, хотя лексикографически оно больше. Числа, записанные как строчки, надо сравнивать не совсем лексикографически.
Vitaly83vvp
14.09.2021 10:03Я, лет 15 назад, начинал писать подобный класс для работы с числами большой длины и операциями над ними. Уже и не помню на чём там остановился, но закончить не получилось из-за недостатка времени. Да, и делал просто для интереса. В таких классах, особое внимание нужно уделять оптимизации количества и сложности операций. Иначе, работа с переменными этих классов будет отнимать много времени. Критично для высоконагруженных систем. Хотя, в простых проектах, с небольшим числом вычислений, может подойти.
8street
14.09.2021 16:39+1Чуть короче.
std::string bignum::getValue() const { if (_isNegative) return "-" + _value; return _value; }
gerdoe
15.09.2021 07:56+1Говорят, если на Хабре разделы комментариев и раздел статьи поменять местами, он станет намного информативнее./s
IosifLvovich
Не знаток C++, но разве в нём не CamelCase для классов используют?
Chuvi
Это реккомендация а не требование
lonely_programmer Автор
Так как это название переменной, я решил написать так, как проще
Saltca
Все таки название класса
avdosev
в стандартной библиотеке в приоритете snake_case и, не уверен как называется, некий c-old-style (например, strlen, strcpy), вот под это подходит bignum