Введение


В данной статье я хочу рассказать о своих «приключениях» при решении задачи по STL, возникшей в ходе работы над небольшим проектом (C++11, Visual Studio 2015).

На первый взгляд, задача выглядела весьма просто. Но при ближайшем рассмотрении:
— в открытых источниках готового решения не нашлось;
— стандартные ООП-подходы на ней «забуксовали»;
— оказалось, что даже для опытного разработчика задача может представлять сложность.

Я приведу несколько вариантов решения. Часть из них я отбросил до реализации, но некоторые были реально написаны. Из одних примеров можно извлечь пользу только типа «смотри и никогда так не делай», другие же вполне могут найти применение на практике.

Постановка задачи


Итак, есть структура-хранилище, одним из полей которой является ассоциативный контейнер std::map из STL:

#include <map>

struct Storage
{
	// ... something

	using Data = std::map<int, double>;
	Data data;

	// ... something
};

Затем создаются несколько экземпляров хранилища, в которые набиваются данные:

void Fill(Storage &storage)
{
	int i;
		for ( i = 0; i < 5; ++i )
		{
			storage.data.insert( {i, i} );
		}
}


int main()
{
	Storage data1, data2;

	Fill(data1);
	Fill(data2);

	return 0;
}

Далее (т.к. разработка структур данных шла параллельно с написанием кода, использующего их) выяснилось, что на самом деле эти хранилища должны быть не совсем одинаковыми: в одних экземплярах требуется сортировка в возрастающем порядке, в других — в убывающем. Т.е. наполнение данными можно (и даже нужно) производить единообразно, но вот использование (извлечение) данных сортированной по умолчанию map приходится писать по-разному (для одних экземпляров — прямым итератором, для других — обратным).

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

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

Вариант 1 — лобовой


Дополняем структуру полем разновидности, а ответственность за выбор корректного варианта возлагаем на пользователя структуры. Ну и немного подслащаем пилюлю — обеспечиваем невозможность изменения разновидности после создания экземпляра:

#include <map>
#include <iostream>


struct Storage
{
	// ... something

	using Data = std::map<int, double>;
	Data data;

	enum Type { forward, reverse };
	const Type type;

	Storage() = delete;
	Storage(Type initType) : type(initType) {};

	// ... something
};


void Fill(Storage &storage)
{
	int i;
		for ( i = 0; i < 5; ++i )
		{
			storage.data.insert( {i, i} );
		}
}


std::ostream& operator<<(std::ostream &stream, const Storage &storage)
{
		if ( storage.type == Storage::forward )
				// Перебор в прямом порядке
				for (const auto &iter : storage.data)
					stream << std::endl << iter.first << " " << iter.second;
		else
				// Перебор в обратном порядке
				for (auto iter = storage.data.crbegin(); iter != storage.data.crend(); ++iter)
					stream << std::endl << iter->first << " " << iter->second;

	return stream;
}


int main()
{
	Storage data1(Storage::forward), data2(Storage::reverse);

	Fill(data1);
	Fill(data2);

	std::cout << data1 << std::endl << data2 << std::endl;

	return 0;
}

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

Вариант 2 — шаблоновый


Ассоциативные контейнеры STL (map в том числе) позволяют указывать класс функтора сравнения. Такая возможность требуется довольно редко, поэтому данный параметр имеет значение по умолчанию std::less. В литературе имеются примеры использования ассоциативных контейнеров с нестандартными функторами (как и примеры написания пользовательских функторов), а в заголовочном файле уже определен подходящий нам функтор std::greater — вот просто бери и пользуйся готовым решением.

Итак, объявляем Storage как шаблон, и определяем для него 2 специализации конкретными значениями. На первый взгляд — красивое сompile-time решение: быстро исполняется, выявляет ошибки на этапе компиляции:

#include <map>
#include <iostream>
#include <functional>


enum Type { ascending, descending };


struct StorageAncestor
{
	// ... something
};


template<Type T> struct Storage : StorageAncestor
{
};


template<> struct Storage<ascending>
{
	using Data = std::map<int, double, std::less<int>>;
	Data data;
};


template<> struct Storage<descending>
{
	using Data = std::map<int, double, std::greater<int>>;
	Data data;
};


template<Type T> void Fill(Storage<T> &storage)
{
	int i;
		for ( i = 0; i < 5; ++i )
		{
			storage.data.insert( {i, i} );
		}
}


template<Type T> std::ostream& operator<<(std::ostream &stream, const Storage<T> &storage)
{
		// Перебор в прямом порядке
		for (const auto &iter : storage.data)
			stream << std::endl << iter.first << " " << iter.second;

	return stream;
}


int main()
{
	Storage<ascending> data1;
	Storage<descending> data2;

	Fill(data1);
	Fill(data2);

	std::cout << data1 << std::endl << data2 << std::endl;

	return 0;
}

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

Кроме того:

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

В результате этот вариант был признан реализуемым, но слишком сложным для кодирования.

Вариант 3 — обманный


Идея заключается в следующем: оставить std::map в покое (т.е. в обоих случаях использовать функтор сравнения по умолчанию std::less, вызывающий operator<), а «полечить» значение, использующееся в качестве ключа.

#include <map>
#include <iostream>


enum Type { ascending, descending };


template<typename T> class FraudulentValue
{
	public:
		FraudulentValue(const T &t, Type initType) : m_value(t), m_type(initType) {};
		bool operator< (const FraudulentValue<T> &comp) const
		{
			return (m_type == ascending) ? (m_value < comp.m_value) : (m_value > comp.m_value);
		};
		operator T() const {return m_value;};
	protected:
		T m_value;
		Type m_type;
		FraudulentValue() = delete;
};


struct Storage
{
	// ... something

	using Data = std::map<FraudulentValue<int>, double>;
	Data data;

	const Type type;

	Storage() = delete;
	Storage(Type initType) : type(initType) {};

	// ... something
};


void Fill(Storage &storage)
{
	int i;
		for ( i = 0; i < 5; ++i )
		{
			storage.data.insert( {FraudulentValue<int>(i, storage.type), i} );
		}
}


std::ostream& operator<<(std::ostream &stream, const Storage &storage)
{
		// Перебор в прямом порядке
		for (const auto &iter : storage.data)
			stream << std::endl << iter.first << " " << iter.second;

	return stream;
}


int main()
{
	Storage data1(ascending), data2(descending);

	Fill(data1);
	Fill(data2);

	std::cout << data1 << std::endl << data2 << std::endl;

	return 0;
}

Понятно, что данный вариант категорически не годится для production, т.к.:

— operator< в одних случаях выполняет сравнение "<", а в других ">" — сногсшибательно неожиданное поведение, ничего не скажешь;
— нет механизма, предотвращающего добавление в map значения с неправильно настроенным ключом, что ожидаемо приведет к порче контейнера.

Вариант 4 — инкапсуляционный


«Инкапсуляция» — размещение в одном классе (или структуре, как в данном случае) и данных, и алгоритмов. Что ж, если затруднения вызваны различиями кода обхода — значит, этот код надо разместить там же, где находится и перебираемый контейнер, и флаг направления.

#include <map>
#include <iostream>
#include <functional>


struct Storage
{
	// ... something

	using Data = std::map<int, double>;
	Data data;

	enum Type { forward, reverse };
	const Type type;

	Storage() = delete;
	Storage(Type initType) : type(initType) {};

	using const_functor = std::function<void(const Data::value_type&)> const;

	void for_each(const_functor &functor) const
	{
			if ( type == Storage::forward )
					// Перебор в прямом порядке
					for (auto &iter : data)
						functor(iter);
			else
					// Перебор в обратном порядке
					for (auto iter = data.crbegin(); iter != data.crend(); ++iter)
						functor(*iter);
	};

	// ... something
};


void Fill(Storage &storage)
{
	int i;
		for ( i = 0; i < 5; ++i )
		{
			storage.data.insert( {i, i} );
		}
}


std::ostream& operator<<(std::ostream &stream, const Storage &storage)
{
	storage.for_each([&](const Storage::Data::value_type &value)
		{
			stream << std::endl << value.first << " " << value.second;
		}
	);

	return stream;
}


int main()
{
	Storage data1(Storage::forward), data2(Storage::reverse);

	Fill(data1);
	Fill(data2);

	std::cout << data1 << std::endl << data2 << std::endl;

	return 0;
}

Чтобы данное решение стало пригодным для production, остается всего-навсего:

— написать семейство алгоритмов перебора — в прямом и в обратном направлении, для conts и не-const функторов;
— скрыть data в private, чтобы исключить возможность использования обычного (в данном случае нежелательного) механизма перебора;
— обеспечить полноценный интерфейс к спрятанному контейнеру, для чего написать обертки для трех-четырех десятков функций и повторить 15 typedef'ов.

Вариант 5 — наследовательный, он же — нереализуемый


Идея данного варианта следующая: унаследоваться от std::map, в результате чего получить доступ к каким-либо protected-механизмам map.

Однако данный вариант оказался нереализуемым по следующим причинам:

— все потенциально полезные механизмы оказались объявлены как private, а не protected (возможно, Microsoft-specific);
— даже если бы в protected нашлось что-нибудь подходящее — вариант полностью зависел бы от конкретной реализации STL;
— наследование от STL-контейнера вообще является очень плохой идеей, т.к. STL-контейнеры не имеют виртуального деструктора, что может привести к утечке памяти.
Однако исследование внутреннего устройства std::map натолкнуло меня на следующий вариант.

Вариант 6 — специфичный


При первоначальном анализе задачи казалось, что важен только класс функтора сравнения (ведь мы указываем класс в качестве параметра шаблона std::map, а как именно контейнер распоряжается этим классом — скрыто где-то в недрах реализации). Однако при изучении исходных кодов STL оказалось, что каждый экземпляр std::map создает и хранит экземпляр функтора сравнения. Таким образом, в основе данной идеи лежит настройка экземпляра функтора сравнения в соответствии с требуемым направлением сортировки.
В Microsoft-реализации STL у std::map присутствует недокументированный нестандартный метод _Getcomp(), одна из версий которого предоставляет доступ по ссылке к экземпляру функтора сравнения, позволяя осуществить изменение его внутреннего состояния (настройку) в соответствии с нашими нуждами.

#include <map>
#include <iostream>


enum SortingType { ascending, descending };
enum CompareType { none, less, greater };


template<typename T> class AdjustableCompare
{
	public:
		AdjustableCompare() : m_type(none) {};
		bool operator()(const T &_Left, const T &_Right)
		{
				if ( m_type == less )
					return (_Left < _Right);
				if ( m_type == greater )
					return (_Left > _Right);
			throw std::runtime_error("AdjustableCompare was not initialized");
		};

		void setTypeOnce(CompareType type)
		{
				if ( m_type != none )
					throw std::runtime_error("AdjustableCompare double set");
			m_type = type;
		};
	private:
		CompareType m_type;
};

struct Storage
{
	// ... something

	using Data = std::map<int, double, AdjustableCompare<int>>;
	Data data;

	Storage() = delete;
	Storage(SortingType initType)
	{
		data._Getcomp().setTypeOnce( (initType == ascending) ? less : greater );
	};

	// ... something
};


void Fill(Storage &storage)
{
	int i;
		for ( i = 0; i < 5; ++i )
		{
			storage.data.insert( {i, i} );
		}
}


std::ostream& operator<<(std::ostream &stream, const Storage &storage)
{
		// Перебор в прямом порядке
		for (const auto &iter : storage.data)
			stream << std::endl << iter.first << " " << iter.second;

	return stream;
}


int main()
{
	Storage data1(ascending), data2(descending);

	Fill(data1);
	Fill(data2);

	std::cout << data1 << std::endl << data2 << std::endl;

	return 0;
}

Такой вариант решения:

— в отличие от рассмотренных до сих пор вариантов, осуществляет именно настройку экземпляра контейнера;
— контейнеры с разными направлениями сортировки имеют одинаковый тип, что позволяет единообразно осуществлять как наполнение контейнера, так и извлечение данных, а также создавать контейнеры таких контейнеров (или указателей на них);
— требует наличия у класса функтора сравнения конструктора по умолчанию (из-за чего CompareType имеет три возможных значения вместо двух, а также расширяется пространство некорректного применения функтора);
— является Microsoft-specific;
— основан на вмешательстве во внутренние механизмы map.

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

Вариант 7 — трюковой, или троянский


В std::map присутствует также стандартная функция key_comp(), возвращающая копию экземпляра функтора сравнения. К сожалению, копия функтора не позволяет изменить внутреннее состояние экземпляра класса, хранящегося в недрах std::map, и не поможет в решении нашей задачи.

Согласны?

Ну что же, смотрите код:

#include <map>
#include <iostream>
#include <memory>


enum SortingType { ascending, descending };
enum CompareType { none, less, greater };


template<typename T> class TrickyCompare
{
	public:
		TrickyCompare() : m_type(new CompareType(none)) {};
		bool operator()(const T &_Left, const T &_Right)
		{
				if ( *m_type == less )
					return (_Left < _Right);
				if ( *m_type == greater )
					return (_Left > _Right);
			throw std::runtime_error("TrickyCompare was not initialized");
		};

		void setTypeOnce(CompareType type)
		{
				if ( *m_type != none )
					throw std::runtime_error("TrickyCompare double set");
			*m_type = type;
		};
	private:
		std::shared_ptr<CompareType> m_type;
};



struct Storage
{
	// ... something

	using Data = std::map<int, double, TrickyCompare<int>>;
	Data data;

	Storage() = delete;
	Storage(SortingType initType)
	{
		data.key_comp().setTypeOnce( (initType == ascending) ? less : greater );
	};

	// ... something
};


void Fill(Storage &storage)
{
	int i;
		for ( i = 0; i < 5; ++i )
		{
			storage.data.insert( {i, i} );
		}
}


std::ostream& operator<<(std::ostream &stream, const Storage &storage)
{
		// Перебор в прямом порядке
		for (const auto &iter : storage.data)
			stream << std::endl << iter.first << " " << iter.second;

	return stream;
}


int main()
{
	Storage data1(ascending), data2(descending);

	Fill(data1);
	Fill(data2);

	std::cout << data1 << std::endl << data2 << std::endl;

	return 0;
}

Почему трюковой? Да потому, что благодаря свойствам «умного указателя» std::shared_ptr нам удалось изменить внутреннее состояние функтора сравнения (а значит, и самого контейнера std::map), воспользовавшись лишь копией экземпляра, причем сделать это безопасно в смысле утечек памяти. В данном примере отсутствие конструктора копирования и оператора присваивания, «корректно обрабатывающих указатели» — это не ошибка кодирования, а именно то, что нужно.

Почему троянский? Потому, что мы дали std::map что-то внешне очень простое и полезное, но на самом деле скрывающее внутри некий сложный механизм с неожиданным поведением, способный повлиять на работу map. Против такой хитрости оказались бессильны казалось бы надежные способы защиты внутреннего состояния map, предусмотренные разработчиками стандарта C++ (да-да, это опасно для работы ассоциативного контейнера, т.к. изменение режима работы функтора после добавления некоторого количества элементов почти гарантированно приведет к порче контейнера). В нашем функторе предусмотрена защита от изменения режима работы после инициализации. Но любая защита может содержать ошибки.

Данный способ уже очень близок к финальному варианту. Там не менее, я не рекомендую его к применению в production — не потому, что знаю о каких-либо его фатальных недостатках, а только лишь потому, что обнаружил более простой и «прямой» способ.

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

Вариант 8 — финальный


Корректное решение задачи, конечно же, оказалось очень простым и элегантным.
Среди десятка конструкторов std::map нашлись несколько, принимающие в качестве аргумента экземпляр функтора сравнения. Похоже, подобное использование map было дальновидно предусмотрено разработчиками стандарта C++, за что честь им и хвала.

#include <map>
#include <iostream>
#include <memory>


template<typename T> class Compare
{
	public:
		enum Type { less, greater };
		Compare() = delete;
		Compare(Type type) : m_type(type) {};
		bool operator()(const T &_Left, const T &_Right) const
		{
				if ( m_type == less )
					return (_Left < _Right);
				else
					return (_Left > _Right);
		};
	private:
		Type m_type;
};


struct Storage
{
	// ... something

	using Data = std::map<int, double, Compare<int>>;
	Data data;

	enum Type { ascending, descending };

	Storage() = delete;
	Storage(Type initType) : data((initType == ascending) ? Compare<int>::less : Compare<int>::greater) {};

	// ... something
};


void Fill(Storage &storage)
{
	int i;
		for ( i = 0; i < 5; ++i )
		{
			storage.data.insert( {i, i} );
		}
}


std::ostream& operator<<(std::ostream &stream, const Storage &storage)
{
		// Перебор в прямом порядке
		for (const auto &iter : storage.data)
			stream << std::endl << iter.first << " " << iter.second;

	return stream;
}


int main()
{
	Storage data1(Storage::ascending), data2(Storage::descending);

	Fill(data1);
	Fill(data2);

	std::cout << data1 << std::endl << data2 << std::endl;

	return 0;
}

Данное решение:

— отлично согласуется как с идеологией STL, так и со стандартом C++;
— полностью выполняет поставленную задачу, в том числе желательный пункт (ничто не мешает созданию и единообразной обработке контейнера таких контейнеров);
— имеет высокий уровень защиты от ошибок использования (в том числе по причине отсутствия конструктора по умолчанию у функтора сравнения).

Вывод


Чем же были обусловлены сложности в решении данной задачи?

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

P.S.: несмотря на наличие побочных эффектов и противопоказаний к применению, все примеры, приведенные в статье, функциональны.

Благодарности


the_1x

Литература


— C. Мейерс — Эффективное использование STL
— Working Draft, Standard for Programming Language C++, N3797. П.п. 23.2.4.2, 23.2.4.12.
Поделиться с друзьями
-->

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


  1. vadikvs
    10.01.2017 19:15

    А паттерн стратегия не подошёл бы для решения этой задачи?


    1. Dubovik_a
      10.01.2017 19:19

      Приведите пример применения — тогда станет понятно. Пока я вижу, что паттерн «стратегия» ничем не лучше, чем пример с инкапсуляцией.


      1. vadikvs
        11.01.2017 12:03

        Накидал в свободное время
        #include <iostream>
        #include <string>
        #include <map>
        
        class StorageVirtual
        {
         public:
            virtual ~StorageVirtual(){}
            virtual std::ostream  &print(std::ostream &stream, const std::map<int, double>  &storage)=0;
        
        };
        
        class PrintAction : public StorageVirtual
        {
        public:
            std::ostream &print(std::ostream& stream,const std::map<int, double>  &storage){
        
                    for (const auto &iter : storage)
                                stream << std::endl << iter.first << " " << iter.second;
                     return stream;
        
                }
        };
        
        class RPrintAction : public StorageVirtual
        {
        public:
            std::ostream &print(std::ostream &stream,const std::map<int, double>  &storage){
        
                for (auto iter = storage.crbegin(); iter != storage.crend(); ++iter)
                            stream << std::endl<< iter->first << " " << iter->second;
                            return stream;
        
            }
        };
        
        class CustomPrintAction : public StorageVirtual
        {
        public:
            std::ostream &print(std::ostream &stream,const std::map<int, double>  &storage){
        
                for (auto iter = storage.crbegin(); iter != storage.crend(); ++iter){
                        if((iter->first)%2 == 0){
                            stream << std::endl<< iter->first << " " << iter->second;
                            }
                }
                return stream;
        
            }
        };
        
        struct Storage
        {
            Storage(StorageVirtual *v): virt(v){}
            using Data = std::map<int, double>;
            Data data;
            StorageVirtual* virt;
            ~Storage() { delete virt; }
        };
        std::ostream & operator << (std::ostream &stream, Storage &str){
        
            return str.virt->print(stream,str.data);
        
        }
        
            void Fill(Storage &storage)
            {
                int i;
                    for ( i = 0; i < 5; ++i )
                    {
                        storage.data.insert( {i, i} );
                    }
            }
        
            int main()
            {
        
                Storage data1(new PrintAction), data2(new RPrintAction), data3 (new CustomPrintAction);
                Fill(data1);
                Fill(data2);
                Fill(data3);
        
                std::cout << data1 << std::endl;
                std::cout<< data2<< std::endl;
                std::cout<< data3<< std::endl;
            }


  1. Ddnn
    10.01.2017 19:22
    +12

    Не хочу никому указывать, но, по-моему, вывод должен быть таким: «Потрудитесь внимательно прочитать документацию к используемым инструментам». Ну правда, первая же ссылка в Google по запросу «std::map comparator» содержит информацию о финальном варианте.


    1. Dubovik_a
      10.01.2017 19:36
      -10

      Во-первых, статья не только об одном финальном способе.
      Во-вторых, правильная формулировка — это уже половина решения. А когда есть только постановка задачи — до формулировки вопроса еще далеко.
      Ну и в-третьих, понять, что один из десятков почти не прокомментированных примеров кода является именно нужным решением, кажется простым только «задним числом».


      1. MooNDeaR
        11.01.2017 04:39
        +4

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


  1. fogone
    10.01.2017 19:50
    +2

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


  1. izvolov
    10.01.2017 19:55
    +6

    #include <functional>
    #include <iostream>
    #include <map>
    
    int main ()
    {
        const auto mg =
            std::map<int, int, std::function<bool (int, int)>>
            (
                {{1, 1}, {2, 2}, {3, 3}},
                std::greater<int>{}
            );
        const auto ml =
            std::map<int, int, std::function<bool (int, int)>>
            (
                {{1, 1}, {2, 2}, {3, 3}},
                std::less<int>{}
            );
    
        const auto mm = {mg, ml};
    
        for (const auto & m: mm)
        {
            for (const auto & p: m)
            {
                std::cout << p.first << ' ' << p.second << std::endl;
            }
            std::cout << std::endl;
        }
    }

    http://coliru.stacked-crooked.com/a/9440e50cac759ba9


    Заголовок спойлера


    1. Dubovik_a
      10.01.2017 20:02
      -1

      Нет, это не является решением — вызов функции Fill из любого из моих примеров не компилируется.


      1. izvolov
        10.01.2017 20:07
        +1

        1. Dubovik_a
          10.01.2017 20:11
          -1

          А, да.
          Если убрать const перед ml и mg — работает.


    1. Dubovik_a
      13.01.2017 01:26

      Кстати, да. Спасибо за код.
      Этот способ более красивый, чем мой финальный. Отсутствие общего предка у std::less и std::greater ставило передо мной непреодолимый забор.


    1. vScherba
      14.01.2017 03:07

      Код плюсую, но что-то в последнее время стало модно размахивать std::function, где уместно и где нет.
      Все-таки компаратор в больших (а лучше во всех) STL-коллекциях должен инлайниться. Полиморфный же std::function не инлайнится и ударит по производительности кода и оптимизациям компилятора. Компаратор — это критический по производительности участок кода. Здесь лучше оставить авторский Compare.


      1. izvolov
        14.01.2017 10:53
        +1

        Первое.
        Производительность стд::функции очень модно ругать. В то же время современные её реализации (в гцц 5+, например) очень неплохи: они не совершают виртуальных вызовов, а также используют так называемую "оптимизацию малых объектов".
        Если уж очень хочется помочь компилятору, то можно заменить стд::функцию на простой указатель на функцию. Но уж точно не брать "авторский" компаратор, который внутри делает лишнюю проверку для выбора порядка, что потенциально ведёт к ошибке предсказания.


        Второе.
        Если действительно нужно добиться высокой производительности, то первое, что нужно сделать — это отказаться от использования стд::мапы.


        1. Dubovik_a
          15.01.2017 22:02

          Набросал тест производительности:

          http://ideone.com/o7XaNy

          Примечания:
          — для онлайн-выполнения repetitions выставил в 200 тыс, для десктопного желательно побольше;
          — тестирование проводится для ключей двух типов: int и std::string порядка 9 — 10 символов длиной;
          — тестируется производительность двух операций: вставка случайных данных и поиск случайных данных (ровно тех же, что до этого были вставлены в map);
          — часть теста, выполняющая поиск, может быть выброшена оптимизатором. Чтобы это предотвратить, для MSVC надо компилировать с опцией «Minimize Size (/O1)».

          Итоги:
          — самый быстрый вариант — конечно же map с настройками по умолчанию;
          — даже параметризация map функтором std::greater немного снижает производительность, т.е. бесплатным не является даже мой вариант №2 (шаблоновый);
          — потеря производительности для алгоритмов, активно использующих функтор сравнения, в среднем составляет от 5 до 25 процентов (на том, на чём мне удалось протестировать);
          — вариант с std::function хоть и красив, но всё же несколько медленне, чем вариант с моим Compare — от пары процентов до 10 процентов.


        1. Dubovik_a
          15.01.2017 22:19

          Если действительно нужно добиться высокой производительности, то первое, что нужно сделать — это отказаться от использования стд:: мапы.


          Может подскажете, какой контейнер позволит сортировать напихиваемые данные быстрее, чем стд:: мапа?


        1. vScherba
          15.01.2017 23:11

          Ну зачем же так голословно теоретизировать. Ну не поленитесь замерить производительность. Вот мой код для тестов:

          Скрытый текст
          #include <iostream>
          #include <functional>
          #include <map>
          #include <chrono>
          
          template<typename T> class Compare
          {
          public:
          	enum Type { less, greater };
          	Compare() = delete;
          	Compare(Type type) : m_type(type) {};
          	bool operator()(const T &_Left, const T &_Right) const
          	{
          		if (m_type == less)
          			return (_Left < _Right);
          		else
          			return (_Left > _Right);
          	};
          private:
          	Type m_type;
          };
          
          inline bool less(int a, int b)
          {
          	return a < b;
          }
          
          inline bool greater(int a, int b)
          {
          	return a > b;
          }
          
          int main()
          {
          	/*auto mg = std::map<int, int, std::function<bool(int, int)>>(
          		std::greater<int>{}
          		);
          	auto ml =
          		std::map<int, int, std::function<bool(int, int)>>(
          			std::less<int>{}
          		);*/
          	
          	/*auto mg = std::map<int, int, Compare<int>>(
          		Compare<int>(Compare<int>::greater)
          		);
          
          	auto ml = std::map<int, int, Compare<int>>(
          		Compare<int>(Compare<int>::less)
          		);*/
          
          	auto mg = std::map<int, int, bool(&)(int, int)>(
          		greater
          		);
          
          	auto ml = std::map<int, int, bool(&)(int, int)>(
          		less
          		);
          
          	for (int i = 0; i < 100000; ++i)
          	{
          		mg.insert({ i, i });
          		ml.insert({ i, i });
          	}
          
          	const auto mm = { mg, ml };
          
          
          	auto start = std::chrono::system_clock::now();
          	int res = 0;
          	for (const auto & m : mm)
          	{
          		for (int i = 0; i < 100000000; ++i)
          		{
          			res += m.at(i % 100000);
          		}
          	}
          	std::cout << std::chrono::duration<double>(std::chrono::system_clock::now() - start).count();
          
          	std::cout << res << std::endl;
          }
          


  1. Ruckus
    10.01.2017 21:32
    +3

    Только мне кажется, что задача вылезает из отсутствия даже общего проектирования?
    Постановка задачи прямо так и говорит «мы писали, писали, а тут вдруг раз и узнали, что вообще надо было писать». Мне кажется, что если бы структура данных и работа с ними были продуманны на ранних этапах, то проблемы бы вообще не возникло, так как реализация была бы вовсе иной.


    1. Dubovik_a
      12.01.2017 16:47

      Обход контейнера — это вопросы реализации. При ревизии кода решили сделать удобнее для потребителя данных. При чем тут проектирование?


      1. Ruckus
        12.01.2017 18:06

        При том, что судя по началу статьи внезапно

        выяснилось, что на самом деле эти хранилища должны быть не совсем одинаковыми
        .
        Если бы это было известно заранее (а при проектировании это должно было всплыть), то вероятно было бы что-то иначе.
        PS Может у меня «ООП головного мозга», но я бы не стал давать во вне map, сделал бы все внутри класса с методами заполнения и доступа. Скорей всего при данной задаче «направление» передавалось бы в конструктор класса, где и создавался бы map с нужными параметрами. Это вполне логично, так как хранится там не только map, а завтра окажется, что один параметр должен зависеть от другого и в паблик их давать нельзя, что вы будете делать? А ведь судя по началу статьи это вполне вероятно.

        Ах да, при чем тут проектирование?


        1. Dubovik_a
          12.01.2017 18:19

          я бы не стал давать во вне map, сделал бы все внутри класса с методами заполнения и доступа


          Это примерно мой вариант про инкапсуляцию. В проекте признан нецелесообразным, т.к. чтобы реализовать его полноценно — надо проделать работы раз в 20 больше. Ради чего? Ради того, чтобы примерно повторить интерфейс усредненного STL-контейнера.

          В случае «сферического кода в вакууме» я тоже соглашусь, что лучше все попрятать и сделать интерфейс, не зависящий от типа контейнера и прочих моментов. Но, заметьте, в статье именно struct. Т.е. это хранище, в котором почти нет private, почти нет кода, а доступ ограничивается на более высоких уровнях (да, оно завернуто еще в 2 уровня хранилищ, потому что именно так построена предметная область).


          1. Ruckus
            12.01.2017 20:01

            Объясните для чего вам повторять интерфейс, если вы используете 2 метода?

            «Почти нет кода» для struct я вообще считаю неверным подходом, хоть многие здесь со мной не согласятся. struct не class и по изначальной реализации в C должен лишь агрегировать данные, никаких методов в нём, ИМХО, быть не должно, а если они есть, то так и назовите class. Иначе в чём по вашему концептуальная разница?

            P.S. Да, про struct, возможно, немного старомодно, но я до сих пор не пойму зачем вообще дали возможность в плюсах добавлять код туда. И ведь не только в плюсах. Очень похоже на оверхед потому, что в результате это дублирование функционала class, что, как мне всегда казалось, все стараются избегать, да и более того не имеет смысла.


            1. Dubovik_a
              12.01.2017 20:22

              P.S. Да, про struct, возможно, немного старомодно, но я до сих пор не пойму зачем вообще дали возможность в плюсах добавлять код туда.

              Полностью согласен.

              struct не class

              В мире кроме черного и белого есть еще много-много промежуточных вариантов. В данном случае в реальном проекте это структура с небольшим количеством кода и небольшим количеством private. До класса по сути ей очень далеко.

              Объясните для чего вам повторять интерфейс, если вы используете 2 метода?

              Еще спросите, зачем мне вообще городить весь этот огород, если на самом деле мне надо всего лишь вывесть в cout несколько повторяющихся чисел.

              Это всего лишь пример. И при использовании данных, хранящихся в этой структуре, мне надо будет не только пройти ее «от и до», но понадобится еще десяток способов использования. На проектирование интерфейса map потрачены тысячи человеко-часов, и я вполне отдаю себе отчет, что ничего подобного я не могу себе позволить (и в данном проекте, и вероятно вообще).

              Кроме того, спрятать map — значит, обрезать возможность использования member-functions, часть из которых эффективнее своих общих аналогов. А это значит — или заложить в архитектуру снижение производительности, или сделать для этих методов обертки. Оба решения — так себе.

              Ну и напоследок — 3 примера кода:

              1.
              int a = 0;
              


              2.
              struct Storage
              {
                int a = 0;
              };
              
              

              3.
              class Storage
              {
              public:
                Storage() : a(0) {};
                ~Storage(){};
                int getA() const {return a;};
                void setA(const int new) {a = new;};
              private:
                int a;
              };
              


              Какой из вариантов лучше?


              1. Ruckus
                12.01.2017 21:16

                Вы не этот код явно применяете, раз у вас там ещё ряд private полей и кода. Может в данном случае такой подход применим, но случай вы описали явно не полностью, а про «общий случай» мы уже договорились. =)
                «Тысячи человеко-часов» потрачены на абстрактную реализацию, которая как правило не применяется полностью. Я не предлагаю вам городить свои алгоритмы, я лишь предлагаю конкретизировать заполнение и чтение дабы оно соответствовало проекту. Вы же хотите писать свою функцию «Fill()» каждый раз, как вам понадобится заполнить map, хотя скорей всего они все будут однотипны, а это снова дублирование кода.
                Да, расскажите что ещё вам нужно от контейнера, кроме как положить туда данные и забрать их? Если есть однотипная обработка, то логично её тоже положить рядом с данными, не так ли?
                Но это всё на самом деле обсуждение пустоты потому, что я не знаю задачи и не представляю что ещё там накручено вокруг этого всего.


                1. Dubovik_a
                  12.01.2017 21:49

                  но случай вы описали явно не полностью

                  Вы ожидали увидеть в статье весь проект? Надо было напустить пыли в глаза, чтобы читатель устал читать пояснения, какой класс для чего нужен, в какой уровень вливает данные и почему он это делает именно так?
                  Суть происходящего описана в статье вполне прозрачно:
                  — производится единообразное наполнение (какая разница — одной функцией Fill, или целой цепочкой классов);
                  — производится почти единообразное использование (в постановке задачи я указал, что использоваться будет много где, т.е. небольшое неудобство умножится, плюс возрастёт риск ошибок);
                  — map не лежит просто голый посреди кода, как int a из прошлого моего коммента, и не запрятан в private класса за кучей оберток, а является именно public-полем структуры. Т.е. есть возможность рядом в этой структуре разместить еще какие-то поля, и даже немного кода (много не хочется, потому что да, оно тогда станет больше похоже на класс).

                  «Тысячи человеко-часов» потрачены на абстрактную реализацию, которая как правило не применяется полностью

                  Весь STL и map в том числе — это хорошо спроектированная библиотека. Приведенный в статье Storage — это в общем-то тоже библиотека, но только «для собственного применения». Если я обеспечу интерфейс только к части функциональности map — я ограничу возможное использование. Лучший способ сделать мне настолько же хорошо — это просто повторить. В данном случае — повторить не реализацию, а интерфейс map. Всё, что я спроектирую сам за разумное время — будет в лучшем случае на две головы ниже, и мне не понятно, почему Вы этого не понимаете, говоря о проектировании.

                  Если потребителя данных не пугает сложность map — то что мне мешает «выпятить» его наружу? Это ни в коем случае не внутренний механизм, позволяющий классу делать какую-то магию и выдавать наружу только её результат. Это — хранилище, которое в одном модуле наполняется, а в другом используется.

                  Если есть однотипная обработка, то логично её тоже положить рядом с данными, не так ли?

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


  1. oleg1977
    11.01.2017 00:31
    +1

    Чем это отличается от https://habrahabr.ru/post/210746/?


    1. Dubovik_a
      13.01.2017 01:21

      Больше вариантов, больше рассуждений, больше пояснений, немного меньше кода. Один и тот же материал можно подать по-разному, не так ли?


  1. Akon32
    11.01.2017 11:51
    +1

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

    А документацию вы читали? Первой же строкой:


    explicit map (const key_compare& comp = key_compare(),
                  const allocator_type& alloc = allocator_type());

    И вообще-то, возможность передать компаратор в сортирующий контейнер или алгоритм — стандартная фича многих языков (точнее, их стандартных и нестандартных библиотек).


    Есть ещё способ перебирать элементы в разных порядках — написать обёртку типа iterator над reverse_iterator (не уверен, что она нужна), и добавить методы my_iterator()/my_begin()/my_end() в контейнер, которые будут возвращать требуемый в зависимости от ситуации итератор.


    1. Dubovik_a
      13.01.2017 01:24

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

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


      1. Akon32
        16.01.2017 17:48

        при чтении хелпов не задействуются почти никакие типы памяти.

        Это где такое написано?
        С моим опытом это не согласуется. Обычно достаточно просмотреть доки, чтобы помнить основные возможности API. Их в т.ч. для этого и пишут.


      1. Dubovik_a
        16.01.2017 18:11
        -1

        Любая информация хорошо запоминается человеческой памятью тогда, когда она обмусолена разными способами со всех сторон, и на этом построена вся индустрия образования (вспомните изучение естествознания в высшем образовании: лекция, практика, лабораторная работа). А когда просто видишь 12 конструкторов… Ну не знаю… Я не робот, и извлек из чтения списка конструкторов очень мало информации.

        Конкретнее: если бы при виде конструктора, принимающего Traits (который почти всегда использовал по умолчанию), я мог бы себе представить: «Ага, это можно использовать так, и еще вот так», и сразу применить это на практике — оно бы запомнилось. А так — просто пролистываешь, отмечая: «Ай, это всё мне не надо, это для каких-то особых случаев».

        Так вот, я уверен, что после нормального (не «по диагонали») прочтения статьи процентов 90 программистов вспомнят этот особый случай хотя бы в общих чертах, и найдут подходящее решение намного быстрее, чем до прочтения.


        1. Akon32
          17.01.2017 12:07

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

          Нет же. Информация запоминается хорошо тогда, когда считается нужной.


          просто пролистываешь, отмечая: «Ай, это всё мне не надо

          Когда вы имели дело с "индустрией образования", может быть, это всё было "не надо" (но, к слову, не везде так).


          Когда вы читаете доки по библиотеке — скорее всего это вам нужно, вне зависимости от того, как вы привыкли в университете или в школе. Вы же их читаете не потому, что вас заставили читать?


          На практике: не запомнили — написали 7 велосипедов.
          Так надо или не надо?


          1. Dubovik_a
            17.01.2017 12:46

            Вы серьезно считаете, что техническая документация усваивается так же хорошо, как и написанная нормальным живым языком книга (статья)?


            1. Akon32
              17.01.2017 15:54

              Считаю, что API часто можно изучать по нормально написанной документации. Это, навскидку, 70-80% библиотек.


  1. Dubovik_a
    12.01.2017 16:16
    -3

    Интересно, если бы Страуструп опубликовал главу готовящейся книги в статье на Хабре — его бы тоже заплевали? Мне кажется, что да, потому что половина комментаторов сказала бы, что он пишет банальные очевидные вещи, которые и так описаны в стандарте, а другая половина сказала бы, что это слишком заумно и в реальной жизни неприменимо.