Я всегда слышал, что с библиотеками в С++ что-то не так, как и с ограничением максимального целочисленного значения, да и вообще то, что язык сложный и непонятный. Что же, сегодня, мы начнём писать собственную библиотеку больших чисел, полностью своими руками 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.

Как создать и настроить проект
image

image

image

В файле заголовка (.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, и смотрим нашу переменную.

image

Отлично, всё работает! По поводу отладчика, это безумно удобный интерфейс. Как сказал один мудрый человек, если программист не умеет пользоваться отладчиком, то этот человек не программист. Краткий экскурс по данному чуду: f5 (запуск отладки), shift+f5 (остановка), ctrl+shift+f5 (перезапуск), f10 (шаг с заходом в функцию), f11 (шаг с обходом функции), shift+f11 (шаг с выходом из функции), f5 (во время отладки, перейти к следующей точке остановы).

▍ Геттеры


Так, теперь создадим функцию геттер, чтобы иметь возможность читать поле нашего класса. Для этого объявим эту функцию в BigNumLib.h (файл заголовок) в поле public:

//@return string
std::string getValue();

Мы написали комментарии в стиле DOC++. Этот тип комментариев понимает Visual Studio и красиво отображает нам. (ключевые слова пишутся через ‘@’: @return, @param)

image

Теперь необходимо прописать логику данных функций в 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;
}

image

▍ Перегрузка конструктора (приём 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. После чего, число переводится в строку и если число отрицательное, то удаляется первый символ из строки (‘-’).

Сейчас мы можем протестировать это и присвоить число. Попробуем присвоить положительное и отрицательное число, посмотрим, правильно ли работает наша программа:

image

Отлично, теперь попробуем ввести огромное число:

image

Как мы можем видеть, 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 параметром).

image

По поводу функции parsStringToBigNumParams(). Данная функция превращает строку, в набор параметров нашего класса. В начале она проверяет, стоит ли ‘-’, на первой позиции в _value, если да, убрать знак из строки и присвоить параметру _isNegative = true. После чего идёт проверка, если в _value найдено, что какой-то элемент не совпадает с цифирным набором (npos — не найдено совпадений), то выкинуть исключение. И дальше присвоить размер.

Что делает исключение? Полностью останавливает работу программы, если его не обработать. Давайте же опробуем работу наших конструкторов и обработаем исключение:

image

Как работает обработка исключений? В блоке try пишется небезопасный код, если в нём происходит ошибка, мы тут же попадаем в блок catch. Из-за того, что мы добавили описание в нашу самодельную ошибку, мы можем увидеть, какое значение, смогло сломать нашу библиотеку.

▍ Заключение


Что же, я чувствую, что выдал достаточно много инфы и, если продолжить, у новичков она может превратиться в кашу. За сегодня мы прошли очень много интересных тем и познакомились с некоторыми особенностями языка С++, но впереди ещё больше крутой информации, такая как перегрузка операторов, указатели, resize string и собственная логика в математических операциях. Если вам заходит такой формат обучения/разработки реального проекта, дайте знать, буду пилить 2 часть в таком же формате, ну, если вы вообще ждёте 2 часть :)
p/s Ссылка на GitHub

Комментарии (52)


  1. IosifLvovich
    13.09.2021 16:27
    +1

    bignum::bignum()

    Не знаток C++, но разве в нём не CamelCase для классов используют?


    1. Chuvi
      13.09.2021 16:39
      +4

      Это реккомендация а не требование


    1. lonely_programmer Автор
      13.09.2021 17:30
      +2

      Так как это название переменной, я решил написать так, как проще


      1. Saltca
        14.09.2021 19:59

        Все таки название класса


    1. avdosev
      13.09.2021 20:55
      +3

      в стандартной библиотеке в приоритете snake_case и, не уверен как называется, некий c-old-style (например, strlen, strcpy), вот под это подходит bignum


  1. sidorovmax
    13.09.2021 16:57
    +13

    "_isNegative = other_value < 0 ? true : false;"
    - это за пределами добра и зла.
    За такое с технического собеседования сразу вылетают.


    1. lonely_programmer Автор
      13.09.2021 17:13
      +1

      Моя задача была показать как можно больше вариантов написания кода. Описание логики через тернарный оператор является одним из таких способов. Данный пример показывает, что и в С++ можно такое реализовывать в одну строчку. +Если это не показывать, новички не будут знать о существовании таких методов.


      1. Kotofay
        13.09.2021 17:58

        А именовать локальную переменную начиная с "_" перед этим проговорив что с "_" начинают приватные члены класса?
        Это намеренное введение в заблуждение.

        Не каждый начинающий не забудет ставить this-> перед членом класса и будет долго думать почему его переменная присваивается не туда.


        1. lonely_programmer Автор
          13.09.2021 18:03

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


      1. TheRikipm
        13.09.2021 19:09
        +9

        ИМХО, демонстрация какой-либо конструкции языка в месте где она не нужна, хуже чем отсутствие её демонстрации вообще. Новичок может решить что подобная запись является нормой и через неуоторое время ему будет сложно перестроиться.


      1. 8street
        14.09.2021 16:24

        del


    1. Kiever1
      14.09.2021 14:37

      А почему это плохо?


      1. bogolt
        14.09.2021 15:46
        +2

        other_value < 0

        уже возвращает bool ( меньше или нет )
        затем мы bool превращем в bool

        если разбить это на 2 строки получится так
        bool isNeg = other_value < 0;
        return isNeg ? true : false; // если isNeg true вернуть true иначе вернуть false
        

        чувствуете некую избыточность?


        1. Kiever1
          14.09.2021 17:10

          Да, точно. Что-то несообразил.

          Наверное меня сбил с толку win32 стиль, там бывают такие преобразования для BOOL.


    1. thecove
      14.09.2021 15:39
      -1

      и что плохого в этой записи?

      ну лично мне режет глаз _isNegative

      я бы так написал:

      const bool isNegative = other_value < 0 ? true : false;


      1. Naf2000
        14.09.2021 19:24
        +1

        Тернарный оператор не нужен. Операция сравнения уже даёт bool


        1. 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;


  1. sergegers
    13.09.2021 17:00
    +15

    На всякий случай, если кто вдруг подумает, что так пишут на C++. Это не так. Никто не будет хранить число в строке и конвертировать его туда и обратно. Есть, например, библиотека Boost.Multiprecision и, уверен, ещё несколько библиотек, которые позволяют работать с большими интами без оверхеда.


    1. lonely_programmer Автор
      13.09.2021 17:21
      +2

      Похоже, вы невнимательно читали статью, если вообще читали. В самом начале, я написал, что библиотека реализована на string переменных и это не есть хорошо. Я написал, что гораздо лучше использовать вместо этого. Предупредил, что многое сделано для упрощения. Да и настоящие математические библиотеки всегда будут быстрее самодельных и я не стремился их обогнать. Моя задача предельно проста - начать с азов объяснять С++ и показывать разношёрстные примеры, как выполнить те или иные действия. Если вы считаете, что статья не подходит для начинающих, я готов вас выслушать.


      1. Kotofay
        13.09.2021 17:27
        +1

        Вы дважды упомянули public и private но ни слова не сказали о том, что стоит за этим, ни одного слова об инкапсуляции. А ведь это один из столпов ООП в С++.


        1. lonely_programmer Автор
          13.09.2021 17:35
          +2

          Я же описал для чего мы используем поля public и private. И немного затронул тему геттеров. getValue() - является одним из таких, где возвращает поле _value прямиком из private. Возможно, нужно было создать 2 класс и показать наследование, чтобы углубиться в ООП, но мне показалось, что это будет перебором.


          1. Kotofay
            13.09.2021 17:52
            +1

            Инкапсуляция к наследованию никак не относится. И 2-й класс действительно будет лишним.

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

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


      1. sergegers
        13.09.2021 19:25
        +1

        Надо было написать вариант для чисел фиксированного размера на шаблонах, конечно. Волноваться не надо, у вас не получится невзначай математическая библиотека. Зато будет продемонстрирована идея, лежащая за такой библиотекой и показаны уникальные свойства C++. Если же вы не в силах сделать это, то разумнее было бы подобрать другой пример.

        Ваше же решение написано на C с классами, может быть перенесено на массу языков типа Java, C#, а главное - идеологически неверно. Я бы сравнил его с поиском по отсортированному массиву последовательным перебором. Начинающих надо учить на упрощённых примерах, конечно, но не на неправильных.


    1. reishi
      14.09.2021 12:11
      -2

      Вы, наверное, и для is-even будете буст подключать?


  1. INSTE
    13.09.2021 17:31
    +1

    Кстати написать c++ binding к libtommath или openssl для bignum было бы куда поучительнее костыляния со строками.


    1. lonely_programmer Автор
      13.09.2021 17:50
      +1

      Что же, сегодня, мы начнём писать собственную библиотеку больших чисел, полностью своими руками c 0, и узнаем, так ли страшен С++, как его малюют?

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


  1. 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 вполне хватило бы.


    1. lonely_programmer Автор
      13.09.2021 18:01
      +1

      1. Я не совсем понял про перекрытие поле класса. Нам же нельзя позволить пользователю изменять данный параметр (поле). Получить его, он может, изменить - нет. На самом деле, эта функция заглушка, в 1 части я просто не успел рассказать про перегрузку операторов потокового вводы/вывода.

      2. В первой ревизии ошибки кидались через специальную функцию my_exeption и было дополнительное поле _isNum. При каждой математической операции данное поле проверялось на true. Спустя время было решено отказаться от данной реализации, по причине громоздкости кода. P.S. вывод ошибки с минусом и правда неверный, я забыл добавить if(_isNegative) добавить минус в начало строки.

      3. Честное слово, у меня такой же вопрос к моей Visual Studio. Ей недостаточно одного string конструктора. На самом деле, гораздо лучше было бы использовать функцию шаблон, но это уже во 2 части.

        Спасибо за отзыв!


      1. xi-tauw
        13.09.2021 19:02

        1. Вопрос в имени переменной. Вы дали одинаковые имена переменной во внутренней области видимости (функция) и внешней (класс). Написали бы проще.

        2. Вам бы еще нормализацию прикрутить к вариантам типа "-00000".

        3. Нет, это вопрос к вам.

        UPD: третий вопрос снимается, я понял, что вы хотели сделать.


  1. NeoCode
    13.09.2021 18:27

    Это чисто учебный проект? Есть же GMP и Boost.Multiprecision который ее использует (хотя в бусте есть и собственная реализация).
    А вообще конечно поддержка длинных чисел должна быть встроена в язык на уровне литералов.


    1. lonely_programmer Автор
      13.09.2021 20:37

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


  1. static_cast
    13.09.2021 20:11
    +1

    Для чисто учебного проекта норм. А вообще, хранить разряды в char-ах неэффективно, так только чуть удобнее для человеческого восприятия при отладке. Обычно переходят к системам счисления, основанным на длине машинного слова. Допустим, с основанием в 32 бита (половина от 64, чтобы было удобнее - избегать обработки переполнения при поразрядных операциях). Это быстрее и занимает меньше памяти.


    1. lonely_programmer Автор
      13.09.2021 20:40

      О неэффективности данного подхода (реализация через string) было сказано в самом начале статьи. Я слышал, что лучше всего использовать булевую алгебру. Ваш подход звучит очень интересно, я бы с удовольствием изучил его.


    1. yeputons
      13.09.2021 20:50

      Если основывать на системе счисления, не являющейся степенью десятки, то придётся переводить в десятичную систему и обратно. Наивный подход будет работать за квадратичное время, получим как в Python или Java: вычисления мгновенные, а вот вывод числа на миллион разрядов невозможен. Является ли это проблемой — зависит от задачи.


      1. static_cast
        13.09.2021 21:42

        Ну, машинная математика, основанная на степени двойки вас же устраивает. =)


        1. yeputons
          13.09.2021 21:52

          Устраивает, потому что вывод работает.

          Если у меня задача, в которой надо зачем-то посчитать большое десятичное число (обычно в развлекательных/учебных целях), то перестаёт устраивать, потому что у меня перестаёт работать вывод ответа на экран. Например, запустите на питоне вот такой код:

          x = 10 ** 1000000 + 4
          print(x % 10)
          print(x)

          Само число x из миллиона знаков посчиталось быстрее, чем за секунду, равно как и его остаток от деления на 10. А вот вывод этого числа целиком занимает некоторое время, потому что идёт перевод из двоичной системы в десятичную неэффективным способом.


          1. static_cast
            13.09.2021 22:08
            +1

            Обычно все-таки вывод занимает незначительную долю в тех расчетах, для которых требуется bignum. И почему тормозит вывод еще нужно смотреть. Особенно в Питоне. Может быть, print зашился на аллокации памяти. Или тормозит какой-нибудь форматтер вывода для bignum. Или, банально, компилятор лениво вычислил x не в момент декларирования операций, а перед использованием, выполняя print(x % 10), а вы решили что это перевод в десятичную систему. Правильный тест зачастую сложнее алгоритма, который он тестирует.


            1. yeputons
              13.09.2021 22:29

              Обычно все-таки вывод занимает незначительную долю в тех расчетах, для которых требуется bignum.

              Да, зависит от задачи, всё верно.

              И почему тормозит вывод еще нужно смотреть

              Он не просто тормозит, он у меня минуту работает. А вот если выводить как print(bin(x)) — то пару секунд.

              Я не спец в CPython, но файл с названием longintrepr выглядит многообещающе: вот тут нам сообщают, что всё действительно хранится в двоичной системе счисления (точнее, с основанием 2**30), а вот тут парой вложенных циклов переводят число из одной системы счисления в другую перед выводом в строку. Даже на Кнута сослались.

              Вывод: вывод длинного числа в CPython работает за квадратичное от длины числа время. 10**12 операций — это не шутка, даже если соптимизировать в десяток-сотню раз.


          1. winwood
            14.09.2021 11:48

            во-первых, какой смысл в печати числа, занимающего несколько страниц? Человек все равно его воспринимает лишь на уровне "ух, какое большое".
            во-вторых, библиотека GMP, которую уже упоминали в комментариях, доступна и в питоне. Добавление к Вашему коду

            import gmpy2
            print(gmpy2.mpz(x))


            творит чудеса - все преобразуется и печатается "мгновенно" )


            1. 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) и ещё кучу неасимптотических оптимизаций сверху. Так что результат вполне ожидаемый.


  1. 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?


    1. Izaron
      18.09.2021 20:58
      +2

      Заклинаю вас, никогда так не делайте и не давайте таких советов.
      https://eel.is/c++draft/lex.name#3.2

      В данном случае нарушения нет, такие символы как члены класса, локальные переменные, методы и переменные внутри блока namespace - не являются находящимися в "global namespace".


  1. yeputons
    13.09.2021 21:05
    +6

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

    Из алгоритмического:

    1. Убрать поле _size — либо оно всегда равно _value.size(), либо это что-то непонятное. Незачем усложнять инвариант класса. Можно забыть случайно обновить, а так код короче станет.

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

    Из C++-специфичного:

    1. Использовать member initialization list в конструкторе вместо переприсваивания полей.

    2. Не начинать переменные с _ — это допустимо для полей, но, например, для глобальных переменных — неопределённое поведение (undefined behavior, UB).

    3. Принимать строки по константным ссылкам, где можно. Или по значению и перемещать. В конструкторе, например, сейчас лишнее копирование.

    4. Можно сделать user-defined literal.

    5. Для тестов удобно взять какую-нибудь библиотеку юнит-тестов вроде onqtam doctest.

    Дальше посмотрел на код вашей библиотеки на GitHub:

    1. Вместо своих методов swap можно использовать стандартный std::swap. К тому же их совершенно незачем делать методами, могли бы быть свободными функциями, причём видимыми только внутри .cpp. А для этого их стоит заключить в unnamed namespace.

    2. Функцию сравнения чисел можно написать гораздо короче и проще, если использовать паттерн вроде if (x != y) return x < y;.

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


    1. lonely_programmer Автор
      13.09.2021 21:38

      Спасибо за такой информативный отзыв. На счёт _size переменой. Она и правда является лишней, спасибо всем, кто подметил это. В своё время, я создал её, потому что думал, что взять значение из _size гораздо выгоднее, чем высчитать str.size().

      Насчет кода на GitHub: проводится масштабна оптимизация. Swap функцию я решил написать в виде typedef функции.

      По поводу if(x!=y) return x<y. Красивое элегантное решение, но оно мне не подойдёт, так как приходится сравнивать числа поцфыорно (если можно так сказать).

      Спасибо большое за отзыв!


      1. yeputons
        13.09.2021 21:54

        Красивое элегантное решение, но оно мне не подойдёт, так как приходится сравнивать числа поцфыорно (если можно так сказать).

        Можно в цикле то же самое написать. Ну и раз уж у вас всё равно цифры хранятся как строчки — то можно сначала сравнить по длине, а потом просто как строчки при помощи <, он поддерживается std::string. Правда, тут надо аккуратно с инвариантами.


        1. 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;
          }
          


          1. yeputons
            13.09.2021 22:37
            +1

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


  1. Vitaly83vvp
    14.09.2021 10:03

    Я, лет 15 назад, начинал писать подобный класс для работы с числами большой длины и операциями над ними. Уже и не помню на чём там остановился, но закончить не получилось из-за недостатка времени. Да, и делал просто для интереса. В таких классах, особое внимание нужно уделять оптимизации количества и сложности операций. Иначе, работа с переменными этих классов будет отнимать много времени. Критично для высоконагруженных систем. Хотя, в простых проектах, с небольшим числом вычислений, может подойти.


  1. 8street
    14.09.2021 16:39
    +1

    Чуть короче.

    std::string bignum::getValue() const
    {
    	if (_isNegative)
    		return "-" + _value; 
    	return _value;
    }


  1. unsignedchar
    14.09.2021 17:00

    Для helloworld неплохо.
    А от пустой строки на входе падать не будет?


  1. PetrEEEfim
    14.09.2021 17:04
    +4

    для учебного проекта вполне себе норм


  1. gerdoe
    15.09.2021 07:56
    +1

    Говорят, если на Хабре разделы комментариев и раздел статьи поменять местами, он станет намного информативнее./s