Используя концепции C++20, я начал использовать их как способ определения «интерфейсов» шаблонных классов, поскольку они создают очень хорошие ошибки компилятора. Однако я немного пытаюсь определить их эргономичным и читабельным образом.
Например, предположим, что я хочу создать концепцию IFoo
, которая статически проверяет наличие в классе следующих трех функций с точными типами ввода/возврата:
struct Foo {
void bar(uint32_t a, uint16_t b, uint8_t c) {}
int32_t baz(int32_t d, int16_t e, uint8_t f) {return 0;}
void bax(char g, uint8_t *h, uint32_t *i) {}
};
Если эта концепция определена правильно, то static_assert(IFoo<Foo>);
скомпилируется. Единственный способ получить такое поведение в «едином» определении концепции заключается в следующем:
template<typename T>
concept IFoo = requires(T ifoo, uint32_t a, uint16_t b, uint8_t c, int32_t d, int16_t e, uint8_t f, char g, uint8_t *h, uint32_t *i) {
{ifoo.bar(a, b, c)} -> std::same_as<void>;
{ifoo.baz(d, e, f)} -> std::same_as<int32_t>;
{ifoo.bax(g, h, i)} -> std::same_as<void>;
};
Это удовлетворяет требованию, однако не особенно читабельно, поскольку вам нужно мысленно переносить (многие) типы из аргументов requires()
на входы функции, чтобы сформировать представление о том, как должны выглядеть прототипы функций, если вы реализуете этот класс. Другой способ добиться этого — определить разные концепции для каждой функции и объединить их в цепочку следующим образом:
template<typename T>
concept IFoo_bar = requires(T ifoo, uint32_t a, uint16_t b, uint8_t c) {
{ifoo.bar(a, b, c)} -> std::same_as<void>;
};
template<typename T>
concept IFoo_baz = requires(T ifoo, int32_t d, int16_t e, uint8_t f) {
{ifoo.baz(d, e, f)} -> std::same_as<int32_t>;
};
template<typename T>
concept IFoo_bax = requires(T ifoo, char g, uint8_t *h, uint32_t *i) {
{ifoo.bax(g, h, i)} -> std::same_as<void>;
};
template<typename T>
concept IFoo = requires {
requires IFoo_bar<T>;
requires IFoo_baz<T>;
requires IFoo_bax<T>;
};
Однако это создает много шаблонного кода, который не идеален. Чего я действительно хочу (что не компилируется), так это возможности вкладывать блоки требований следующим образом:
template<typename T>
concept IFoo = requires(T ifoo) {
requires(uint32_t a, uint16_t b, uint8_t c) {
{ifoo.bar(a, b, c)} -> std::same_as<void>;
};
requires(int32_t d, int16_t e, uint8_t f) {
{ifoo.baz(d, e, f)} -> std::same_as<int32_t>;
};
requires(char g, uint8_t *h, uint32_t *i) {
{ifoo.bax(g, h, i)} -> std::same_as<void>;
};
};
Я что-то пропустил? Или пути, которые я уже изучил, являются единственным способом достичь того, чего я хочу? Очень надеюсь, что я что-то упустил в документации, что облегчит задачу!
поскольку a
/d
относятся к одному типу, вы можете избавиться от одного из них.
Н.б. вы не проверяете точные типы параметров, а только аргументы этих типов неявно преобразуются в эти типы параметров.
@Caleth, на самом деле полезно помнить об этом, может показаться, что я пытаюсь написать «прототип функции» с помощью концепции, но это совершенно не то, чем концепция является на самом деле.
@haydenridd: Мне кажется, если бы вы использовали реальные осмысленные имена параметров, это было бы намного читабельнее. a
ничего не говорит и не имеет значения.
std::same_as<void>
- это вообще антипаттерн. Для непустоты следует делать convertible_to
, а для непустоты вас это не должно волновать. Кто-то, создающий метод с совместимыми аргументами и совместимыми возвращаемыми значениями, должен удовлетворить ваш контракт. Концепции должны проверять использование, а не реализацию.
Вы можете объединить их с помощью &&
:
template<typename T>
concept IFoo =
requires(T ifoo, uint32_t a, uint16_t b, uint8_t c) {
{ifoo.bar(a, b, c)} -> std::same_as<void>;
} &&
requires(T ifoo, int32_t d, int16_t e, uint8_t f) {
{ifoo.baz(d, e, f)} -> std::same_as<int32_t>;
} &&
requires(T ifoo, char g, uint8_t *h, uint32_t *i) {
{ifoo.bax(g, h, i)} -> std::same_as<void>;
};
Хороший! Как ни странно, я пытался сделать это раньше, но, должно быть, испортил синтаксис, получилось что-то приятное для глаз!
Вам не нужно называть каждый параметр в предложении require.
struct Foo {
void bar(int n) {}
};
template<typename T>
concept IFoo = requires(T foo) {
{foo.bar(0)} -> std::same_as<void>;
};
работает отлично. Если у вас есть тип, значения которого сконструировать немного сложнее, вы можете просто использовать declval
struct Foo {
void bar(Unmentionable n) {}
};
template<typename T>
concept IFoo = requires(T foo) {
{foo.bar(std::declval<Unmentionable>())} -> std::same_as<void>;
};
все параметры в requires
по сути являются просто сокращением declval
, вам вообще не нужно их писать:
template<typename T>
concept IFoo = requires {
{std::declval<T>().bar(std::declval<Unmentionable>())} -> std::same_as<void>;
};
Однако, если вам действительно нравится синтаксис requires
, вы можете вложить их, как в исходном вопросе:
template<typename T>
concept IFoo = requires(T ifoo) {
requires requires(uint32_t a, uint16_t b, uint8_t c) {
{ifoo.bar(a, b, c)} -> std::same_as<void>;
};
requires requires(int32_t d, int16_t e, uint8_t f) {
{ifoo.baz(d, e, f)} -> std::same_as<int32_t>;
};
requires requires(char g, uint8_t *h, uint32_t *i) {
{ifoo.bax(g, h, i)} -> std::same_as<void>;
};
};
У вас просто было одно requires
слишком мало (тупой двойной requires
появляется, потому что он вводит вложенное требование, и выражение requires
можно поместить туда; следовательно, requires requires
.
Потрясающий! Это именно то, что я искал. Мне нравится сочетать это с другим комментарием об использовании конструкторов типов в вызове функции для указания типа, а не ввода в предложение require, а затем использовать declval<> для более сложных/определяемых пользователем типов.
@Cubic: «работает отлично» Нет, это не так. 0
— константа нулевого указателя. Таким образом, функция, принимающая указатель любого типа, будет удовлетворять этому ограничению.
@NicolBolas И? Вопрос был в синтаксисе, а не в том, как ужесточить условия require (которые в этом примере уже были довольно расплывчатыми).
@Cubic Да, и, как отмечает Никол, использование 0
— плохой выбор синтаксиса (потому что оно делает требования совершенно неясными). Если вы хотите просто сказать «передать целое число», 1
в этом отношении намного лучше.
Ну, я понимаю, что вы хотите избежать шаблонного кода. Вы также можете избегать сторонних библиотек, хотя я собираюсь показать вам одну (мою собственную, если вы хотите ее использовать - довольно короткую для того, что она предлагает, эффективно только с одним заголовком и полностью документированную здесь). Он предоставляет полный набор простых в использовании шаблонов для определения характеристик функций, таких как типы параметров, количество параметров, тип возвращаемого значения, является ли нестатическая функция-член «константной», «изменчивой» и т. д. Таким образом, он дает вам способность действительно сосредоточиться на своей задаче. Конечно, вы можете захотеть придерживаться своих текущих методов или, возможно, найти какой-то гибрид, который вас устроит (смесь ваших собственных и библиотечных). Однако следующий пример демонстрирует его возможности. Считаете ли вы, что это излишне для ваших конкретных потребностей, это, конечно, ваш выбор. Как уже отмечалось, вы также можете использовать другие шаблоны библиотеки, чтобы определить, являются ли функции «константными», «изменчивыми», «noException», невариативными (нет «...» в списке параметров) и т. д., просто выполнив команду добавление этих проверок к концепциям подписи ниже («HasBarSignature», «HasBazSignature» и «HasBaxSignature»). Обратите внимание, что, возможно, этот код даже удастся сократить, но это всего лишь мой первый быстрый подход к нему (но он все еще довольно чистый, несмотря на его длинный вид).
К сожалению, нет времени должным образом документировать код, но сама библиотека тщательно документирована (см. ссылку выше). Также обратите внимание, что предполагается, что перегрузки ваших функций не существуют, иначе «IFoo» вернет false (поскольку вызов «&T::NAME», видимый в макросе DECLARE_HAS_MEMBER_FUNCTION, не может устранить неоднозначность перегрузок, ограничение самого языка - преодолеть его невозможно. выполнимо без приведения, чтобы выбрать именно ту перегрузку, которая вас интересует, но более длинную историю). В любом случае, я надеюсь, что это поможет.
Нажмите здесь, чтобы запустить его.
// Standard C++ headers
#include <cstdint>
#include <iostream>
////////////////////////////////////////////////////////
// See https://github.com/HexadigmAdmin/FunctionTraits
// Only header required from this library ...
////////////////////////////////////////////////////////
#include "TypeTraits.h"
/////////////////////////////////////////////////////
// Boiler plate macro you can stick in some generic
// header (will leave for you to figure out)
/////////////////////////////////////////////////////
#define DECLARE_HAS_MEMBER_FUNCTION(NAME) \
template <typename, typename = void> \
struct HasMemberFunction_##NAME : std::false_type \
{ \
}; \
template <typename T> \
struct HasMemberFunction_##NAME<T, std::enable_if_t<std::is_member_function_pointer_v<decltype(&T::NAME)>>> : std::true_type \
{ \
}; \
template <typename T> \
inline constexpr bool HasMemberFunction_##NAME##_v = HasMemberFunction_##NAME<T>::value;
////////////////////////////////////////////////
// Implementation code for all your interface
// concepts ("IFoo" only in this example).
////////////////////////////////////////////////
namespace Private
{
namespace Concepts
{
////////////////////////////////////////////////////
// Packaging up the "IFoo" implementation in this
// namespace. You can repeat this code for each of
// your interfaces (customizing it for each one
// accordingly)
////////////////////////////////////////////////////
namespace IFoo
{
///////////////////
// Function "bar"
///////////////////
namespace Bar
{
DECLARE_HAS_MEMBER_FUNCTION(bar);
///////////////////////////////////////////////////
// See these "StdExt" templates at
// https://github.com/HexadigmAdmin/FunctionTraits
///////////////////////////////////////////////////
template <typename T>
concept HasBarSignature = StdExt::IsVoidReturnType_v<T> &&
StdExt::ArgCount_v<T> == 3 &&
std::is_same_v< StdExt::ArgType_t<T, 0>, uint32_t > &&
std::is_same_v< StdExt::ArgType_t<T, 1>, uint16_t> &&
std::is_same_v< StdExt::ArgType_t<T, 2>, uint8_t >;
template <typename T>
concept HasBar = HasMemberFunction_bar_v<T> &&
HasBarSignature<decltype(&T::bar)>;
}
///////////////////
// Function "baz"
///////////////////
namespace Baz
{
DECLARE_HAS_MEMBER_FUNCTION(baz);
template <typename T>
concept HasBazSignature = std::is_same_v< StdExt::ReturnType_t<T>, int32_t > &&
StdExt::ArgCount_v<T> == 3 &&
std::is_same_v< StdExt::ArgType_t<T, 0>, int32_t > &&
std::is_same_v< StdExt::ArgType_t<T, 1>, int16_t > &&
std::is_same_v< StdExt::ArgType_t<T, 2>, uint8_t >;
template <typename T>
concept HasBaz = HasMemberFunction_baz_v<T> &&
HasBazSignature<decltype(&T::baz)>;
}
///////////////////
// Function "bax"
///////////////////
namespace Bax
{
DECLARE_HAS_MEMBER_FUNCTION(bax);
template <typename T>
concept HasBaxSignature = StdExt::IsVoidReturnType_v<T> &&
StdExt::ArgCount_v<T> == 3 &&
std::is_same_v< StdExt::ArgType_t<T, 0>, char > &&
std::is_same_v< StdExt::ArgType_t<T, 1>, uint8_t * > &&
std::is_same_v< StdExt::ArgType_t<T, 2>, uint32_t * >;
template <typename T>
concept HasBax = HasMemberFunction_bax_v<T> &&
HasBaxSignature<decltype(&T::bax)>;
}
}
}
}
template <typename T>
concept IFoo = Private::Concepts::IFoo::Bar::HasBar<T> &&
Private::Concepts::IFoo::Baz::HasBaz<T> &&
Private::Concepts::IFoo::Bax::HasBax<T>;
// Sample struct from your post
struct Foo {
void bar(uint32_t a, uint16_t b, uint8_t c) {}
int32_t baz(int32_t d, int16_t e, uint8_t f) {return 0;}
void bax(char g, uint8_t *h, uint32_t *i) {}
};
int main()
{
// true
std::cout << std::boolalpha << IFoo<Foo>;
return 0;
}
они вам не нужны
{ifoo.bar(uint32_t{},uint16_t{},uint8_t{})} -> std::same_as<void>;
, в основном это stackoverflow.com/a/71614974/4117728