Идиома именованного параметра времени компиляции с constexpr

Недавно я столкнулся с довольно большим количеством ситуаций, в которых идиома именованных параметров была бы полезна, но я бы хотел, чтобы это было гарантировано во время компиляции. Стандартный метод возврата ссылок в цепочке почти всегда вызывает конструктор времени выполнения (компилируется с Clang 3.3 -O3).

Я не смог ничего найти со ссылкой на это, поэтому я попытался заставить это работать с constexpr и получил что-то функциональное:

class Foo
{
private:
int _a;
int _b;
public:
constexpr Foo()
: _a(0), _b(0)
{}
constexpr Foo(int a, int b)
: _a(a), _b(b)
{}
constexpr Foo(const Foo & other)
: _a(other._a), _b(other._b)
{}
constexpr Foo SetA(const int a) { return Foo(a, _b); }
constexpr Foo SetB(const int b) { return Foo(_a, b); }
};
...
Foo someInstance = Foo().SetB(5).SetA(2); //works

Хотя это нормально для небольшого числа параметров, для больших чисел это быстро превращается в беспорядок:

    //Unlike Foo, Bar takes 4 parameters...
constexpr Bar SetA(const int a) { return Bar(a, _b, _c, _d); }
constexpr Bar SetB(const int b) { return Bar(_a, b, _c, _d); }
constexpr Bar SetC(const int c) { return Bar(_a, _b, c, _d); }
constexpr Bar SetD(const int d) { return Bar(_a, _b, _c, d); }

Есть ли способ лучше? Я смотрю на это с классами, которые имеют много (более 30) параметров, и кажется, что это будет подвержено ошибкам при расширении в будущем.

РЕДАКТИРОВАТЬ: Удален тег C ++ 1y — хотя C ++ 1y, по-видимому, устраняет проблему (спасибо TemplateRex!), Это для производственного кода, и мы застряли на C ++ 11. Если это означает, что это невозможно, то я думаю, что так оно и есть.

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

HardwareConfiguration hardwareConfig = {
.portA = HardwareConfiguration::Default,
.portB = 0x55,
...
};

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

HardwareConfiguration hardwareConfig = HardwareConfiguration()
.SetPortA( Port().SetPolarity(Polarity::ActiveHigh) )
.SetPortB( Port().SetPolarity(Polarity::ActiveLow) );

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

4

Решение

Использование шаблонного метапрограммирования

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

Пример определения класса и его использование

Вот пример того, что определение класса повлечет за собой с вашей стороны:

template <
//Declare your fields here, with types and default values
typename PortNumber = field<int, 100>,
typename PortLetter = field<char, 'A'>
>
struct MyStruct : public const_obj<MyStruct, PortNumber, PortLetter>  //Derive from const_obj like this, passing the name of your class + all field names as parameters
{
//Your setters have to be declared like this, by calling the Set<> template provided by the base class
//The compiler needs to be told that Set is part of MyStruct, probably because const_obj has not been instantiated at this point
//in the parsing so it doesn't know what members it has. The result is that you have to use the weird 'typename MyStruct::template Set<>' syntax
//You need to provide the 0-based index of the field that holds the corresponding value
template<int portNumber>
using SetPortNumber = typename MyStruct::template Set<0, portNumber>;

template<int portLetter>
using SetPortLetter = typename MyStruct::template Set<1, portLetter>;

template<int portNumber, char portLetter>
using SetPort = typename MyStruct::template Set<0, portNumber>
::MyStruct::template Set<1, portLetter>;//You getters, if you want them, can be declared like this
constexpr int GetPortNumber() const
{
return MyStruct::template Get<0>();
}

constexpr char GetPortLetter() const
{
return MyStruct::template Get<1>();
}
};

Используя класс

int main()
{
//Compile-time generation of the type
constexpr auto myObject =
MyStruct<>
::SetPortNumber<150>
::SetPortLetter<'Z'>();

cout << myObject.GetPortNumber() << endl;
cout << myObject.GetPortLetter() << endl;
}

Большая часть работы выполняется const_obj шаблон. Он предоставляет механизм для изменения вашего объекта во время компиляции. Очень похоже на Tupleдоступ к полям осуществляется с помощью индексов, основанных на 0, но это не мешает вам обернуть сеттеры понятными именами, как это делается с помощью SetPortNumber и SetPortLetter выше. (Они просто ждут, чтобы установить<0> и установить<1>)

О хранилище

В текущей реализации, после того, как все сеттеры были вызваны и объект объявлен, поля в конечном итоге сохраняются в компактном массиве const unsigned charназван data в базовом классе. Если вы используете поля, которые не являются символами без знака (как, например, в PortNumber, как это было сделано выше), поле делится на big endien unsigned chars (может быть изменен на little endien при необходимости). Если вам не нужно фактическое хранилище с фактическим адресом памяти, вы можете вообще его опустить, изменив packed_storage (см. полную ссылку на реализацию ниже), и значения по-прежнему будут доступны во время компиляции.

Ограничения

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

template<int portNumber, char portLetter>
using SetPort = typename MyStruct::template Set<0, portNumber>::
MyStruct::template Set<1, portLetter>;

Полный код

Полный код для реализации этой маленькой библиотеки можно найти здесь:

Полная реализация

Дополнительные примечания

Этот код был протестирован и работает с реализацией C ++ 11 как g ++, так и clang.
Он не тестировался часами и часами, поэтому, конечно, могут быть ошибки, но он должен дать вам хорошую базу для начала. Надеюсь, это поможет!

4

Другие решения

В C ++ 14 ограничения на constexpr функция будет ослаблена, и обычная цепочка установщиков, возвращающих ссылки, будет работать во время компиляции:

#include <iostream>
#include <iterator>
#include <array>
#include <utility>

class Foo
{
private:
int a_ = 0;
int b_ = 0;
int c_ = 0;
int d_ = 0;

public:
constexpr Foo() = default;

constexpr Foo(int a, int b, int c, int d)
:
a_{a}, b_{b}, c_{c}, d_{d}
{}

constexpr Foo& SetA(int i) { a_ = i; return *this; }
constexpr Foo& SetB(int i) { b_ = i; return *this; }
constexpr Foo& SetC(int i) { c_ = i; return *this; }
constexpr Foo& SetD(int i) { d_ = i; return *this; }

friend std::ostream& operator<<(std::ostream& os, const Foo& f)
{
return os << f.a_ << " " << f.b_ << " " << f.c_ << " " << f.d_ << " ";
}
};

int main()
{
constexpr Foo f = Foo{}.SetB(5).SetA(2);
std::cout << f;
}

Живой пример используя Clang 3.4 соединительную линию SVN с std=c++1y,

Я не уверен, что классы с 30 параметрами являются хорошей идеей (принцип единой ответственности и все такое), но, по крайней мере, приведенный выше код линейно масштабируется по количеству сеттеров, имея только 1 аргумент на сеттер. Также обратите внимание, что есть только 2 конструктора: по умолчанию (который берет свои аргументы из инициализаторов в классе) и полный, который занимает 30 дюймов в вашем конечном случае).

3

По вопросам рекламы [email protected]