LINUX.ORG.RU

Карта фабрик на лямбдах: clang не осилил

 , , ,


1

6

Появилась задача: распарсить конфиг и на его основе инициализировать сложную иерахию полиморфных объектов. Можно было накатать простыню if-else на пару страниц или написать генератор этой самой простыни, но я выбрал велосипединг.

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

Привожу очень упрощенный вариант:

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

template <typename TKey, typename TProduct, typename... Args>
struct Factory {

	using Product = std::unique_ptr<TProduct>;
	using Lambda = std::function<Product(Args...)>;
	using Map = std::map<TKey, Lambda>;

	template <typename TConcreteProduct>
	static Lambda create;
};


template <typename TKey, typename TProduct, typename... Args>
template <typename TConcreteProduct>
typename Factory<TKey, TProduct, Args...>::Lambda
Factory<TKey, TProduct, Args...>::create = [](Args... args) {
	return std::make_unique<TConcreteProduct>(args...);
};

struct Base {

	Base(int number, const std::string & message) :
		_number(number), _message(message) {}
	virtual ~Base() = default;
	virtual void print() const = 0;

protected:

	int _number;
	std::string _message;
};

struct First : Base {
	using Base::Base;
	virtual void print() const override {
		std::cout << "First::print() = " << _number << ", " << _message << "\n";
	}
};

struct Second : Base {
	using Base::Base;
	virtual void print() const override {
		std::cout << "Second::print() = " << _number << ", " << _message << "\n";
	}
};

struct Third : Base {
	using Base::Base;
	virtual void print() const override {
		std::cout << "Third::print() = " << _number << ", " << _message << "\n";
	}
};

using SimpleFactory = Factory<std::string, Base, int, const std::string &>;

static const SimpleFactory::Map factoryMap({
	{ "first", SimpleFactory::create<First> },
	{ "second", SimpleFactory::create<Second> },
	{ "third", SimpleFactory::create<Third> },
});

int main() {

	std::cout << "using a specific factory:\n";
	auto key = "first";
	auto factory = factoryMap.at(key);
	auto product = factory(123, "some message");
	std::cout << key << ": ";
	product->print();
	std::cout << "\n";

	std::cout << "test all:\n";
	for(auto & factoryPair : factoryMap) {
		auto product = factoryPair.second(321, "test123");
		std::cout << factoryPair.first << ": ";
		product->print();
	}

}

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

clang 4.0.0-r2:

using a specific factory:
first: Third::print() = 123, some message

test all:
first: Third::print() = 321, test123
second: Third::print() = 321, test123
third: Third::print() = 321, test123

gcc 5.4.0-r3:

using a specific factory:
first: First::print() = 123, some message

test all:
first: First::print() = 321, test123
second: Second::print() = 321, test123
third: Third::print() = 321, test123

Ответ на: комментарий от annulen

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

Nietzsche
() автор топика

Есть ощущение, что ваш код только gcc нормально и соберется. VC++, например, не будет компилировать. Ругается на то, что вот в этом месте:

template <typename TKey, typename TProduct, typename... Args>
template <typename TConcreteProduct>
typename Factory<TKey, TProduct, Args...>::Lambda
Factory<TKey, TProduct, Args...>::create = [](Args... args) {
	return std::make_unique<TConcreteProduct>(args...); // <-- HERE!
};
тип TConcreteProduct не определен. Что, в принципе, логично. Т.к. тело лямбды, которое используется для инициализации Factory::create к шаблону отношения не имеет.

Ну и вызывает недоумение то, что вы хотите достичь. Вы объявляете в шаблоне Factory статический член класса, который зависит еще от одного шаблона. Грубо говоря, пытаетесь сделать вот это:

template<typename T> struct A {
  template<typename U> static int b;
};
Получается, что если в программе есть, например, A<int>, то в ней может быть любое число A<int>::b. Скажем, A<int>::b<int>, A<int>::b<long>, A<int>::b<string> и т.д. Что довольно странно. И, думается мне, что если C++ такое и допускает, то все эти варианты должны где-то явным образом инстанцироваться и инициализироваться.

Если есть желание получить кросс-платформенный код, который не будет зависеть от компилятора и его версии, то можно поступить проще:

template <typename TKey, typename TProduct, typename... Args>
struct Factory {

	using Product = std::unique_ptr<TProduct>;
	using Lambda = std::function<Product(Args...)>;
	using Map = std::map<TKey, Lambda>;

	template <typename TConcreteProduct>
	static Lambda make_lambda() {
		return [](Args... args) {
			return std::make_unique<TConcreteProduct>(args...);
		};
	}
};
...
static const SimpleFactory::Map factoryMap{
	{ "first", SimpleFactory::make_lambda<First>() },
	{ "second", SimpleFactory::make_lambda<Second>() },
	{ "third", SimpleFactory::make_lambda<Third>() },
};
В таком виде работает и под VC++ в Windows, и под clang-4.0.0 + gcc-6.3.1 под Linux-ом.

eao197 ★★★★★
()
Ответ на: комментарий от eao197

Как часто и бывает, наиболее разумное решение оказывается проще, понятнее и очевиднее.

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

Кстати, clang на код из шапки даже не пискнул - что-то с ним определенно не так.

Nietzsche
() автор топика
Ответ на: комментарий от Nietzsche

Кстати, clang на код из шапки даже не пискнул - что-то с ним определенно не так.

Если в варианте для VC++ сделать что-то вроде:

template <typename TKey, typename TProduct, typename... Args>
struct Factory {

	using Product = std::unique_ptr<TProduct>;
	using Lambda = std::function<Product(Args...)>;
	using Map = std::map<TKey, Lambda>;

	template <typename TConcreteProduct>
	static Lambda create;

	template <typename TConcreteProduct>
	static Lambda make_lambda() {
		return [](Args... args) {
			return std::make_unique<TConcreteProduct>(args...);
		};
	}
};


template <typename TKey, typename TProduct, typename... Args>
template <typename TConcreteProduct>
typename Factory<TKey, TProduct, Args...>::Lambda
Factory<TKey, TProduct, Args...>::create = Factory::make_lambda<TConcreteProduct>();
То тоже не пикает, но в run-time оказывается, что все create содержат нулевые указатели внутри.

Полагаю, и у clang-а и у msvc есть проблемы с инстанциированием статического шаблонного члена в шаблонном классе. Оба делают всего один такой экземпляр, только clang его переинициализирует постоянно (поэтому в нем и оказывается фабрика для Third, ведь это последняя созданная фабрика). А msvc вообще не инициализирует.

Тогда как gcc справляется с этой ситуацией. Наверное, gcc все-таки прав, а шаблонный член шаблонного класса — это такое темное место в стандарте, до нормальной реализации которого другие компиляторы еще не добрались.

eao197 ★★★★★
()
Ответ на: комментарий от Nietzsche

Тут сказалась моя шаблонофобия

У тебя странная шаблонофобия: лепишь шаблонные лямбды вместо указателя на статический create:

    template <typename TConcreteProduct>
    static std::unique_ptr<TProduct> create(Args... args) {
        return std::make_unique<TConcreteProduct>(args...);
    }
Будет работать везде.

ЗЫ: впервые вижу variable templates и они кажутся крайне мутными, хотя это больше похоже на ошибку в clang:

    printf("1 => %p\n2 => %p\n3 => %p\n", &SimpleFactory::create<First>, &SimpleFactory::create<Second>, &SimpleFactory::create<Third>); // 3 different pointers

kawaii_neko ★★★★
()

Карта фабрик

А чо не компас фабрик?

anonymous
()
Ответ на: комментарий от eao197

VC++, например, не будет компилировать.

У MSVC++ проблемы с шаблонами, он их обрабатывает не по стандарту. Не вылезло ли это здесь?

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

fixed

anonymous
()
Ответ на: комментарий от eao197

Что, в принципе, логично. Т.к. тело лямбды, которое используется для инициализации Factory::create к шаблону отношения не имеет.

А что там насчёт dependent names?

anonymous
()
21 июля 2017 г.
Ответ на: комментарий от eao197

тип TConcreteProduct не определен.

А это template <typename TConcreteProduct> что?

anonymous
()
5 марта 2019 г.

Есть мнение что проблема в unspecified порядке инициализации статических переменных.

anonymous
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.