Динамические мат. функции в C++

Здравствуйте, Хабраюзеры.
Недавно я прочитал здесь статью об анонимных функциях в С++, и тут же у меня в голове возникла мысль: нужно срочно написать класс для работы с функциями, которые нам знакомы из математики. А именно, принимающими вещественный аргумент и возвращающими вещественное же значение. Нужно дать возможность обращаться с такими объектами максимально просто, не задумываясь о реализации.
И вот, как я это реализовал.

Проблема.

Все эти лямбда-выражения ведут себя порой довольно странно, по крайней мере для меня. Связано это, вероятно, с тем, что я не могу до конца понять, как устроен механизм создания этих выражений. Создавать большие функции я буду на основе уже имеющихся примитивными действиями, т.е. что-то вроде f(x) = a(x) + b(x). А это значит, что все лямбды, созданные даже лишь как промежуточные звенья в построении функций, должны сохраняться для обращения к ним. Сейчас попробую объяснить попонятнее.

Скажем, мы хотим, чтобы некая процедура принимала пару функций A и B и возвращала новое выражение, например, 5A + B. Наша процедура создаст лямбду 5A, затем создаст 5A + B, используя 5А и В. Полученное процедура вернет и завершится, в этот момент лямбда 5А пропадет из области видимости, и возвращенное выражение попросту не будет работать.

Решение.

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

#include <xstddef>
#include <functional>
#include <list>

typedef std::tr1::function<double(double)> realfunc; // y = f(x) as in maths

class func_t
{
protected:
	static std::list<realfunc> all_functions; //коллекция всех функций

	realfunc *f; //указатель на элемент коллекции

public:
	func_t();
	func_t(const double);
	func_t(const realfunc&);
	func_t(const func_t&);

	~func_t() {};

	friend func_t operator+ (const func_t&, const func_t&);
	friend func_t operator- (const func_t&, const func_t&);
	friend func_t operator* (const func_t&, const func_t&);
	friend func_t operator/ (const func_t&, const func_t&);
	friend func_t operator^ (const func_t&, const func_t&);

	func_t operator() (const func_t&);

	double operator() (const double);

};


Для начала конструкторы.
Конструктор по умолчанию (без параметров) будет создавать функцию, возвращающую аргумент. Это будет точка отправления. f(x) = x.
Остальные понятны: второй создает функцию-константу — f(x) = c, третий превращает лямбду нужного типа в объект моего класса, последний — просто конструктор копирования.

Реализация конструкторов, по ней сразу будет видно, как устроены все методы класса:

func_t::func_t()
{
	f = &(*all_functions.begin());
}

func_t::func_t(const double c)
{
	func_t::all_functions.push_back(
		[=](double x)->double {return c;}
	);
	this->f = &all_functions.back();
}

func_t::func_t(const realfunc &realf)
{
	func_t::all_functions.push_back(realf);
	this->f = &all_functions.back();
}

func_t::func_t(const func_t &source)
{
	this->f = source.f;
}


Как видите, я создаю лямбду, толкаю ее в конец коллекции и возвращаю объект с указателем на нее.
Хочу сразу пояснить первый же конструктор. Как я уже говорил, создание «аргумента», т.е. функции f(x)=x является началом почти любой работы с моим классом, поэтому я решил особо выделить эту функцию и положил в первую же ячейку коллекции это выражение. И тогда при вызове конструктора по умолчанию объект всегда получает указатель на первый элемент коллекции.

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

Далее, операторы. С ними всё просто. Покажу три из них: первый реализует сложение, второй композицию функций, третий оценку.

func_t operator+ (const func_t &arg, const func_t &arg2)
{
	realfunc realf = [&](double x)->double {
		return (*arg.f)(x) + (*arg2.f)(x);
	};
	return func_t(realf);
}

func_t func_t::operator() (const func_t &arg)
{
	realfunc realf = [&](double x)->double {
		return (*f)((*arg.f)(x));
	};
	return func_t(realf);
}

double func_t::operator() (const double x)
{
	return (*f)(x);
}


Всё просто, правда? В первых двух опять создается нужная лямбда, а потом посылается в конструктор объекта. В третьем методе вообще халява =)
Поясню, что я везде использую передачу окружения по ссылкам (это [&] перед лямбдой) для доступа к аргументам метода.

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

protected:
	static class func_t_static_init_class
	{ public: func_t_static_init_class(); };
	static func_t_static_init_class func_t_static_init_obj;

...

//Static:
std::list<realfunc> func_t::all_functions = std::list<realfunc>();
func_t::func_t_static_init_class func_t::func_t_static_init_obj = func_t::func_t_static_init_class();

func_t::func_t_static_init_class::func_t_static_init_class()
{
	func_t::all_functions.push_back(
		[](double x)->double {return x;}
	);
}


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

Бонусы.

В принципе, вот и всё. Осталась пара вещей, которые я делал уже чисто для интереса (хотя, собственно, как и всё).

Во-первых, перегрузим парочку функций из cmath.
friend func_t sin(const func_t&);
friend func_t cos(const func_t&);
friend func_t tan(const func_t&);
friend func_t abs(const func_t&);

...

func_t sin(const func_t& arg)
{
	realfunc realf = [&](double x)->double {
		return sin((*arg.f)(x));
	};
	return func_t(realf);
}


Во-вторых, куда ж без производных и первообразных =)

static double delta_x;

func_t operator~ ();
func_t operator| (const double);

...

double func_t::delta_x = 0.01;

func_t func_t::operator~ ()
{
	realfunc realf = [&](double x)->double {
		return ((*f)(x + delta_x / 2) - (*f)(x - delta_x / 2)) / delta_x;
	};
	return func_t(realf);
}

func_t func_t::operator| (double first_lim)
{

	realfunc realf = [=](double x)->double {
		double l_first_lim = first_lim; //will move with this copy of first_lim
		double area = 0;
		bool reverse = x < first_lim; //first_lim > second_lim?
		if (reverse) {
			l_first_lim = x;
			x = first_lim;
		}
		double l_delta_x = delta_x; //step
		while (l_first_lim < x) { //move along the whole span
			if ((l_first_lim += l_delta_x) > x) //stepped too far?
				l_delta_x += x - l_first_lim; //the last l_delta_x may be shorter
			/* integral summ, the point is chosen between
			   the point for f(x) is chosen between l_first_lim and l_first_lim + l_delta_x */
			area += l_delta_x * (*f)(l_first_lim + l_delta_x / 2);
		}
		return area * (reverse?-1:1);
	};
	return func_t(realf);

}


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

Заключение.

Ну вот, собственно, и всё. Надеюсь, что было интересно. Не знаю, имеет ли что-то такое хоть какой-то смысл, но я попрактиковался в классах, всяких & и *, а для меня это главное =) Спасибо за внимание.

Упс! Еще кое-что.

Ну да, как это использовать.
Например вот так:
func_t f1 = cos(5 * func_t() + 8);

это создаст, как видно, функцию
f1(x) = cos(5x + 8)

или вот так:
funt_t x = func_t();
func_t f = x + f1(x / ~f1);

это f(x) = x + f1(x / f1`(x)) кэп мимо проходил

или в конце концов так:
realfunc g = [](double->double) {
    ...
    //что вашей душе угодно, хоть сокеты создавайте
    ...
}
func_t f3 = g;

Заключение номер 2.

Ну теперь точно всё. Еще раз спасибо за внимание!
Если что, полный и недоделанный код тут.
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 34

    +2
    ЗдОрово. А еще можно переписать без лямбд, тогда код будет работать на компиляторах без поддержки C++11. Вот на примере оператора сложения:

    func_t operator+ (const func_t &arg, const func_t &arg2)
    {
        struct _sum
        {
            _sum(const func_t &arg, const func_t &arg2)
                : _arg(arg), _arg2(arg2){}
    
            double operator()(double x)
            {
                return (*_arg.f)(x) + (*_arg2.f)(x);
            }
    
            const func_t& _arg;
            const func_t& _arg2;
        };
    
        realfunc realf = _sum(arg, arg2);
        return func_t(realf);
    }
    

    Хотя, коряво все это выглядит без лямбд.
      +1
      Долой переменные!
      Виват Функциональное!
      Виват Бесточечное!

      *(еще вот такое есть — похоже на ваше)
        +2
        А статический список, если я правильно понял, используется только для вечного хранения realfunc объектов, он всегда растет при конструировании func_t и очищается только по завершении программы? Я бы посмотрел в сторону shared_ptr и хранил бы его вместо realfunc *f, если нет циклических ссылок. Список был бы не нужен.
          0
            0
            shared_ptr<realfunc> f;
            
            Угловые скобки съелись.
              0
              Да, это тот факт, с которым я смирился перед тем, как начать писать. Можно, конечно, создавать внешние классы для «цепочек» составления функций. Т.е., я решаю в программе какую-то задачу в созданном специально под эту задачу объекте, а потом удаляю его.
              Выглядело бы это примерно так:
              func_chain T1();
              T1::func_t f = ...
              ...
              

              Но всё равно со статической коллекцией, пусть не одной.

              К сожалению, я не имею ни малейшего представления о shared_ptr.
                +1
                Самое время заиметь.
              +2
              Я конечно польщён, что моя статья про лямбды натолкнула Вас на такую идею, однако, интуиция мне подсказывает, что можно было всё это сделать проще.

              Я правильно понял, что Вы создаёте статическое хранилище функций, которое заполняете в рантайме? о_О Причём, коллекция только заполняется — из неё ничего не удаляется.

              Значит, если я создам в своей программе 500 функций, то там будет как минимум 500 объектов (на самом деле больше, т.к. при композиции создаются дополнительные объекты func_t), причём удалятся они только при завершении работы программы.

              Как минимум, неплохо бы заменить std::vector<> на std::list<>, а в класс добавить счётчик ссылок, чтобы ненужные функции оттуда удалялись, когда они становятся ненужными.

              А вообще, не могли бы Вы ещё раз объяснить (желательно на примере кода) вот этот абзац, ибо мне кажется, я чего-то не уловил:
              Скажем, мы хотим, чтобы некая процедура принимала пару функций A и B и возвращала новое выражение, например, 5A + B. Наша процедура создаст лямбду 5A, затем создаст 5A + B, используя 5А и В. Полученное процедура вернет и завершится, в этот момент лямбда 5А пропадет из области видимости и возвращенное выражение попросту не будет работать.
                0
                Оу, у Вас и так list<>. Это я провтычил, но ничего от этого не меняется.
                  +1
                  Мне кажется, если поверхностно смотреть, то не нужен list вообще, если вставить shared_ptr на место
                  realfunc *f; //указатель на элемент коллекции
                  
                  в class func_t.

                  Может, я чего-то не учел.
                    0
                    В целом да, должно работать.
                  0
                  #include <iostream>
                  #include <xstddef>
                  #include <functional>
                  
                  using namespace std;
                  
                  typedef tr1::function<double(double)> realfunc;
                  
                  realfunc go(realfunc &A, realfunc &B)
                  {
                  	realfunc A5 = [&](double x) {
                  		return A(x) * 5;
                  	};
                  	realfunc A5pB = [&](double x) {
                  		return A5(x) + B(x);
                  	};
                  	return A5pB;
                  }
                  
                  int main()
                  {
                  	realfunc A = [&](double x) {
                  		return x;
                  	};
                  	realfunc B = [&](double x) {
                  		return x;
                  	};
                  	realfunc A5pB = go(A, B);
                  	cout << A5pB(3) << endl;
                  	getchar();
                  }
                  


                  Вот тут рантайм. А если go() возвращает A5, то всё в порядке.

                  =(
                    +1
                    #include <cstddef>
                    #include <functional>
                    #include <iostream>
                    
                    using namespace std;
                    
                    typedef function<double(double)> realfunc;
                    
                    realfunc go(realfunc A, realfunc B)
                    {
                    	return [=](double x) {
                    		return A(x) * 5 + B(x);
                    	};
                    }
                    
                    int main()
                    {
                    	realfunc A = [](double x) {
                    		return x;
                    	};
                    	realfunc B = [](double x) {
                    		return x;
                    	};
                    	auto A5pB = go(A, B);
                    	cout << A5pB(3) << endl;
                    
                    	return EXIT_SUCCESS;
                    }
                    
                    
                      0
                      Ну так вы в фукнции go() создаете единственный объект и его же копию возвращаете. В моем примере проблема была в потере А5.
                        +1
                        Сделайте лямбду [=] вместо [&] и всё будет работать.
                          0
                          Да, но идея передавать везде все по копированию мне изначально не понравилась. Например, я создам одну гигантскую функцию и сотню маленьких, использующих ее. В моем случае гигансткая будет в одном экземпляре в коллекции, а в вашем, наверное, не в одном.
                          Сейчас вы скажете, что я совершенно не понимаю механизма всех этих штук, и я не возражу.
                            +1
                            Однозначного ответа я Вам не дам, но не исключаю вариант, что компилятор оптимизирует подобные места за счёт Copy Elision хотя бы.
                              +1
                              Попробуйте оба варианта в релизе с максимальной настройкой оптимизации компилятора, сделайте замеры производительности на хорошо нагруженном примере и сравните. Если [=] окажется не менее производительным, чем [&], выбрасывайте список, код упростится.
                                0
                                Размер функции не зависит от размера ее кода, только от того, что она захватывает. И проще сделать так, чтобы гигантская функция захватывала по ссылке нужные данные (и shared_ptr лучше static), а не ее саму захватывать по ссылке.
                                Зато в случае захвата = будет рядом храниться все что надо и это будет быстрее. Память дешевая, дорого обходятся кэш-промахи и локальность данных важнее.
                                А лучший подход — boost phoenix, если динамичность не нужна
                      0
                      Вот если бы оно умело какие-нибудь простейшие преобразования типа a — a = 0, a + 0 = a, a * (b + c) = a * c + b * c. Коли речь зашла о производных, то было бы полезно выводить производные элементарных функций, а не пытаться их аппроксимировать, да еще и не очень точно.
                        0
                        Ну, преобразование «а — а => 0» работало бы только если это было бы одно и то же «а». Если это были бы разные экземпляры одной и той же мат. функции, выяснить такое мне не представляется возможным. Как и проверить, является ли произвольная функция нулем, чтобы сократить ее при сложении.
                        +2
                        Это крайне похоже на Expression Templates, которые используются давно (лет 15) и замечательно обходятся без лямбд.
                          –3
                          Название статьи прочитал как «Динамический мат». Задумался
                            0
                            Мне кажется, вместо указателя удобнее использовать итератор, можно будет тогда и из списка удалять при необходимости. И, как уже говорили, посмотрите в сторону shared_ptr.
                            Вот конструктор копирования угнетает. Зачем Вы еще одну копию кидаете в список? Можно ведь просто скопировать себе указатель.
                              +1
                              В констукторе копирования я как раз просто копирую указатель, не трогая список.
                                0
                                И правда, извиняюсь, перепутал с конструктором от realf.
                              0
                              [quote]
                              Скажем, мы хотим, чтобы некая процедура принимала пару функций A и B и возвращала новое выражение, например, 5A + B. Наша процедура создаст лямбду 5A, затем создаст 5A + B, используя 5А и В. Полученное процедура вернет и завершится, в этот момент лямбда 5А пропадет из области видимости, и возвращенное выражение попросту не будет работать.
                              [/quote]

                              Можно создать лямбду 5A внутри результирующей 5A + B, тогда этой проблемы не будет.
                                0
                                Я даю пользователю класса свободу действий. Пусть делает как хочет, и будет работать.
                                  +1
                                  Да просто скажите прямо — в плюсах не работают замыкания.
                                    0
                                    По ссылке не работают конечно. И это хорошо.
                                      0
                                      Всё работает так, как и должно работать.
                                0
                                Математические функции еще отличаются чистотой, а переменные — иммутабельностью. Короче говоря берите любой функциональный язык и наслаждайтесь.
                                  0
                                  >Ну да, как это использовать.
                                  >Например вот так:
                                  >func_t f1 = cos(5 * func_t() + 8);
                                  >это создаст, как видно, функцию
                                  >f1(x) = cos(5x + 8)

                                  Для тренировки полезно: пока все это заработает, надо достаточно много тонких моментов вспомнить. Только в реальных проектах этим лучше не пользоваться. Лучше:

                                  double f1 (double x) { return cos(5*x + 8); }

                                  =)
                                    0
                                    Ну так это не динамически же.

                                  Only users with full accounts can post comments. Log in, please.