Sfinae c что это

от admin

Jean Guegant

Software Engineer — C++, security, game development and random thoughts.

An introduction to C++’s SFINAE concept: compile-time introspection of a class member

Posted on Sat 31 October 2015 in C++

Trivia:

As a C++ enthusiast, I usually follow the annual C++ conference cppconf or at least try to keep myself up-to-date with the major events that happen there. One way to catch up, if you can’t afford a plane ticket or the ticket, is to follow the youtube channel dedicated to this conference. This year, I was impressed by Louis Dionne talk entitled «C++ Metaprogramming: A Paradigm Shift». One feature called is_valid that can be found in Louis’s Boost.Hana library particulary caught my attention. This genious is_valid function heavily rely on an even more «magic» C++ programming technique coined with the term SFINAE discovered at the end of the previous century. If this acronym doesn’t speak to you, don’t be scared, we are going to dive straight in the subject.

Note: for the sake of your sanity and the fact that errare humanum est, this article might not be 100% accurate!

Introspection in C++?

Before explaining what is SFINAE, let’s explore one of its main usage: introspection. As you might be aware, C++ doesn’t excel when it comes to examine the type or properties of an object at runtime. The best ability provided by default would be RTTI. Not only RTTI isn’t always available, but it also gives you barely more than the current type of the manipulated object. Dynamic languages or those having reflection on the other hand are really convenient in some situations like serialization.

For instance, in Python, using reflection, one can do the following:

As you can see, during serialization, it comes pretty handy to be able to check if an object has an attribute and to query the type of this attribute. In our case, it permits us to use the serialize method if available and fall back to the more generic method str otherwise. Powerful, isn’t it? Well, we can do it in plain C++!

Here is the C++14 solution mentionned in Boost.Hana documentation, using is_valid:

As you can see, it only requires a bit more of boilerplate than Python, but not as much as you would expect from a language as complexe as C++. How does it work? Well if you are too lazy to read the rest, here is the simplest answer I can give you: unlike dynamically typed languages, your compiler has access a lot of static type information once fired. It makes sense that we can constraint your compiler to do a bit of work on these types! The next question that comes to your mind is «How to?». Well, right below we are going to explore the various options we have to enslave our favorite compiler for fun and profit! And we will eventually recreate our own is_valid.

The old-fashioned C++98-way:

Whether your compiler is a dinosaur, your boss refuses to pay for the latest Visual Studio license or you simply love archeology, this chapter will interest you. It’s also interesting for the people stuck between C++11 and C++14. The solution in C++98 relies on 3 key concepts: overload resolution, SFINAE and the static behavior of sizeof.

Overload resolution:

A simple function call like «f(obj);«» in C++ activates a mechanism to figure out which f function shoud be called according to the argument obj. If a set of f functions could accept obj as an argument, the compiler must choose the most appropriate function, or in other words resolve the best overload! Here is a good cppreference page explaining the full process: Overload resolution. The rule of thumb in this case is the compiler picks the candidate function whose parameters match the arguments most closely is the one that is called. Nothing is better than a good example:

In C++ you also have some sink-hole functions that accept everything. First, function templates accept any kind of parameter (let’s say T). But the true black-hole of your compiler, the devil variable vacuum, the oblivion of the forgotten types are the variadic functions. Yes, exactly like the horrible C printf.

The fact that function templates are less generic than variadic functions is the first point you must remember!

Note: A templated function can actually be more precise than a normal function. However, in case of a draw, the normal function will have the precedence.

SFINAE:

I am already teasing you with the power for already few paragraphs and here finally comes the explanation of this not so complex acronym. SFINAE stands for Substitution Failure Is Not An Error. In rough terms, a substitution is the mechanism that tries to replace the template parameters with the provided types or values. In some cases, if the substitution leads to an invalid code, the compiler shouldn’t throw a massive amount of errors but simply continue to try the other available overloads. The SFINAE concept simply guaranties such a «sane» behavior for a «sane» compiler. For instance:

All the expressions won’t lead to a SFINAE. A broad rule would be to say that all the substitutions out of the function/methods body are «safes». For a better list, please take a look at this wiki page. For instance, a wrong substitution within a function body will lead to a horrible C++ template error:

The operator sizeof:

The sizeof operator is really a nice tool! It permits us to returns the size in bytes of a type or an expression at compilation time. sizeof is really interesting as it accurately evaluates an expression as precisely as if it were compiled. One can for instance do:

But wait! If we can manipulate some compile-time integers, couldn’t we do some compile-time comparison? The answer is: absolutely yes, my dear reader! Here we are:

Combining everything:

Now we have all the tools to create a solution to check the existence of a method within a type at compile time. You might even have already figured it out most of it by yourself. So let’s create it:

The reallyHas struct is kinda tricky but necessary to ensure that serialize is a method and not a simple member of the type. You can do a lot of test on a type using variants of this solution (test a member, a sub-type. ) and I suggest you to google a bit more about SFINAE tricks. Note: if you truly want a pure compile-time constant and avoid some errors on old compilers, you can replace the last value evaluation by: «enum < value = sizeof(test (0)) == sizeof(yes) >;«.

You might also wonder why it doesn’t work with inheritence. Inheritence in C++ and dynamic polymorphism is a concept available at runtime, or in other words, a data that the compiler won’t have and can’t guess! However, compile time type inspection is much more efficient (0 impact at runtime) and almost as powerful as if it were at runtime. For instance:

Last but no least, our test cover the main cases but not the tricky ones like a Functor:

The trade-off for a full coverage would be the readability. As you will see, C++11 shines in that domain!

Time to use our genius idea:

Now you would think that it will be super easy to use our hasSerialize to create a serialize function! Okay let’s try it:

It might be hard to accept, but the error raised by your compiler is absolutely normal! If you consider the code that you will obtain after substitution and compile-time evaluation:

Your compiler is really a good guy and won’t drop any dead-branch, and obj must therefore have both a serialize method and a to_string overload in this case. The solution consists in spliting the serialize function into two different functions: one where we solely use obj.serialize() and one where we use to_string according to obj’s type. We come back to an earlier problem that we already solved, how to split according to a type? SFINAE, for sure! At that point we could re-work our hasSerialize function into a serialize function and make it return a std::string instead of compile time boolean. But we won’t do it that way! It’s cleaner to separate the hasSerialize test from its usage serialize.

We need to find a clever SFINAE solution on the signature of «template <class T> std::string serialize(const T& obj)«. I bring you the last piece of the puzzle called enable_if.

As you can see, we can trigger a substitution failure according to a compile time expression with enable_if. Now we can use this failure on the «template <class T> std::string serialize(const T& obj)» signature to dispatch to the right version. Finally, we have the true solution of our problem:

Two details worth being noted! Firstly we use enable_if on the return type, in order to keep the paramater deduction, otherwise we would have to specify the type explicitely «serialize<A>(a)«. Second, even the version using to_string must use the enable_if, otherwise serialize(b) would have two potential overloads available and raise an ambiguity. If you want to check the full code of this C++98 version, here is a gist. Life is much easier in C++11, so let’s see the beauty of this new standard!

Note: it’s also important to know that this code creates a SFINAE on an expression («&C::serialize«). Whilst this feature wasn’t required by the C++98 standard, it was already in use depending on your compiler. It trully became a safe choice in C++11.

When C++11 came to our help:

After the great century leap year in 2000, people were fairly optimistic about the coming years. Some even decided to design a new standard for the next generation of C++ coders like me! Not only this standard would ease TMP headaches (Template Meta Programming side-effects), but it would be available in the first decade, hence its code-name C++0x. Well, the standard sadly came the next decade (2011 ==> C++11), but it brought a lot of features interesting for the purpose of this article. Let’s review them!

decltype, declval, auto & co:

Do you remember that the sizeof operator does a «fake evaluation» of the expression that you pass to it, and return gives you the size of the type of the expression? Well C++11 adds a new operator called decltype. decltype gives you the type of the of the expression it will evaluate. As I am kind, I won’t let you google an example and give it to you directly:

declval is an utility that gives you a «fake reference» to an object of a type that couldn’t be easily construct. declval is really handy for our SFINAE constructions. cppreference example is really straightforward, so here is a copy:

The auto specifier specifies that the type of the variable that is being declared will be automatically deduced. auto is equivalent of var in C#. auto in C++11 has also a less famous but nonetheless usage for function declaration. Here is a good example:

As you can see, auto permits to use the trailing return type syntax and use decltype coupled with an expression involving one of the function argument. Does it means that we can use it to test the existence of serialize with a SFINAE? Yes Dr. Watson! decltype will shine really soon, you will have to wait for the C++14 for this tricky auto usage (but since it’s a C++11 feature, it ends up here).

constexpr:

C++11 also came with a new way to do compile-time computations! The new keyword constexpr is a hint for your compiler, meaning that this expression is constant and could be evaluate directly at compile time. In C++11, constexpr has a lot of rules and only a small subset of VIEs (Very Important Expression) expressions can be used (no loops. )! We still have enough for creating a compile-time factorial function:

constexpr increased the usage of std::true_type & std::false_type from the STL. As their name suggest, these types encapsulate a constexpr boolean «true» and a constrexpr boolean «false». Their most important property is that a class or a struct can inherit from them. For instance:

Blending time:
First solution:

In cooking, a good recipe requires to mix all the best ingredients in the right proportions. If you don’t want to have a spaghetti code dating from 1998 for dinner, let’s revisit our C++98 hasSerialize and serialize functions with «fresh» ingredients from 2011. Let’s start by removing the rotting reallyHas trick with a tasty decltype and bake a bit of constexpr instead of sizeof. After 15min in the oven (or fighting with a new headache), you will obtain:

You might be a bit puzzled by my usage of decltype. The C++ comma operator «,» can create a chain of multiple expressions. In decltype, all the expressions will be evaluated, but only the last expression will be considered for the type. The serialize doesn’t need any changes, minus the fact that the enable_if function is now provided in the STL. For your tests, here is a gist.

Second solution:

Another C++11 solution described in Boost.Hanna documentation and using std::true_type and std::false_type, would be this one:

This solution is, in my own opinion, more sneaky! It relies on a not-so-famous-property of default template parameters. But if your soul is already (stack-)corrupted, you may be aware that the default parameters are propagated in the specialisations. So when we use hasSerialize<OurType>::value, the default parameter comes into play and we are actually looking for hasSerialize<OurType, std::string>::value both on the primary template and the specialisation. In the meantime, the substitution and the evaluation of decltype are processed and our specialisation has the signature hasSerialize<OurType, std::string> if OurType has a serialize method that returns a std::string, otherwise the substitution fails. The specialisation has therefore the precedence in the good cases. One will be able to use the std::void_t C++17 helper in these cases. Anyway, here is a gist you can play with!

I told you that this second solution hides a lot of complexity, and we still have a lot of C++11 features unexploited like nullptr, lambda, r-values. No worries, we are going to use some of them in C++14!

The supremacy of C++14:

According to the Gregorian calendar in the upper-right corner of my XFCE environment, we are in 2015! I can turn on the C++14 compilation flag on my favorite compiler safely, isn’t it? Well, I can with clang (is MSVC using a maya calendar?). Once again, let’s explore the new features, and use them to build something wonderful! We will even recreate an is_valid, like I promised at the beggining of this article.

auto & lambdas:
Return type inference:

Some cool features in C++14 come from the relaxed usage of the auto keyword (the one used for type inference).

Now, auto can be used on the return type of a function or a method. For instance:

It works as long as the type is easily «guessable» by the compiler. We are coding in C++ after all, not OCaml!

A feature for functional lovers:

C++11 introduced lambdas. A lambda has the following syntax:

A useful example in our case would be:

C++14 brings a small change to the lambdas but with a big impact! Lambdas accept auto parameters: the parameter type is deduced according the argument. Lambdas are implemented as an object having an newly created unnamed type, also called closure type. If a lambda has some auto parameters, its «Functor operator» operator() will be simply templated. Let’s take a look:

More than the lambda itself, we are interested by the generated unnamed type: its lambda operator() can be used as a SFINAE! And as you can see, writing a lambda is less cumbersome than writing the equivalent type. It should remind you the beggining of my initial solution:

And the good new is that we have everything to recreate is_valid, right now!

The making-of a valid is_valid:

Now that we have a really stylish manner to generate a unnamed types with potential SFINAE properties using lambdas, we need to figure out how to use them! As you can see, hana::is_valid is a function that takes our lambda as a parameter and return a type. We will call the type returned by is_valid the container. The container will be in charge to keep the lambda’s unnamed type for a later usage. Let’s start by writing the is_valid function and its the containter:

The next step consists at extending container with the operator operator() such as we can call it with an argument. This argument type will be tested against the UnnamedType! In order to do a test on the argument type, we can use once again a SFINAE on a reacreated ‘UnnamedType’ object! It gives us this solution:

If you are a bit lost at that point, I suggest you take your time and re-read all the previous example. You have all the weapons you need, now fight C++!

Our hasSerialize now takes an argument, we therefore need some changes for our serialize function. We can simply post-pone the return type using auto and use the argument in a decltype as we learn. Which gives us:

FINALLY. We do have a working is_valid and we could use it for serialization! If I were as vicious as my SFINAE tricks, I would let you copy each code pieces to recreate a fully working solution. But today, Halloween’s spirit is with me and here is gist. Hey, hey! Don’t close this article so fast! If you are true a warrior, you can read the last part!

For the fun:

There are few things I didn’t tell you, on purpose. This article would otherwise be twice longer, I fear. I highly suggest you to google a bit more about what I am going to speak about.

Firstly, if you wish to have a solution that works with the Boost.Hana static if_, you need to change the return type of our testValidity methods by Hana’s equivalents, like the following:

The static if_ implementation is really interesting, but at least as hard as our is_valid problem solved in this article. I might dedicate another article about it, one day!

Did you noticed that we only check one argument at a time? Couldn’t we do something like:

Actually we can, using some parameter packs. Here is the solution:

This code is working even if my types are incomplete, for instance a forward declaration, or a normal declaration but with a missing definition. What can I do? Well, you can insert a check on the size of your type either in the SFINAE construction or before calling it: «static_assert( sizeof( T ), «type is incomplete.» );«.

Читать:
Как узнать размер базы данных mysql

Finally, why are using the notation «&&» for the lambdas parameters? Well, these are called forwarding references. It’s a really complex topic, and if you are interested, here is good article about it. You need to use «auto&&» due to the way declval is working in our is_valid implementation!

Notes:

This is my first serious article about C++ on the web and I hope you enjoyed it! I would be glad if you have any suggestions or questions and that you wish to share with me in the commentaries.

Anyway, thanks to Naav and Superboum for rereading this article and theirs suggestions. Few suggestions were also provided by the reddit community or in the commentaries of this post, thanks a lot guys!

SFINAE — это просто

Здравствуйте, коллеги.
Хочу рассказать о SFINAE, интересном и очень полезном (к сожалению*) механизме языка C++, который, однако, может представляться неподготовленному человеку весьма мозгоразрывающим. В действительности принцип его использования достаточно прост и ясен, будучи сформулирован в виде нескольких чётких положений. Эта заметка рассчитана на читателей, обладающих базовыми знаниями о шаблонах в C++ и знакомых, хотя бы шапочно, с C++11.
* Почему к сожалению? Хотя использование SFINAE — интересный и красивый приём, переросший в широко используемую идиому языка, гораздо лучше было бы иметь средства, явно описывающие работу с типами.

Сначала, на всякий случай, очень коротко скажу о метапрограммировании в C++. Метапрограммирование — это операции, производимые во время компиляции программы. Подобно тому, как обычные функции позволяют получать значения, при помощи метафункций получают типы и константы времени компиляции. Одно из наиболее популярных применений метапрограммирования, хотя далеко не единственное — узнавать свойства типов. Всё это, разумеется, применяется при разработке шаблонов: где-то бывает полезно знать, имеем мы дело со сложным пользовательским классом с нетривиальным конструктором или с обычным int , где-то необходимо установить, унаследован ли один тип от другого, или можно ли преобразовать один тип в другой. Мы рассмотрим механизм применения SFINAE на классическом примере: проверке того, существует ли в классе функция-член с заданными типами аргументов и возвращаемого значения. Я постараюсь подробно и детально пройти по всем этапам создания проверочной метафункции и проследить откуда что берётся.

  • Когда речь заходит о SFINAE, это обязательно связано с перегрузкой функций.
  • Это работает при автоматическом выводе типов шаблона (type deduction) по аргументам функции.
  • Некоторые перегрузки могут отбрасываться в том случае, когда их невозможно инстанциировать из-за возникающей синтаксической ошибки; компиляция при этом продолжается как ни в чём не бывало, без ошибок.
  • Отбросить могут только шаблон.
  • SFINAE рассматривает только заголовок функции, ошибки в теле функции не будут пропущены.

Рассмотрим простой пример:

Функция difference отлично работает для целых аргументов. А вот с пользовательскими типами данных начинаются тонкости. Результат вычитания не всегда имеет тот же самый тип, что и операнды. Так, разность двух дат — интервал времени, который сам по себе датой не является. Если пользовательский тип MyDate имеет внутри себя определение typedef MyInterval difference_type; и оператор вычитания MyInterval operator — (const MyDate& rhs) const; , к нему применима шаблонная перегрузка. Вызов difference(date1, date2) сможет «увидеть» и шаблонную перегрузку, и версию, принимающую int , при этом шаблонная перегрузка будет сочтена более подходящей.
Тип MyString , в котором нет difference_type , при подстановке вызовет ошибку: функция возвращала бы несуществующий тип. Вызов difference с аргументами типа MyString сможет «увидеть» только int -версию функции. Эта единственная версия окажется достаточно подходящей только если в MyString определён оператор преобразования в число. Конструкция val1 — val2 требует наличия бинарного оператора «минус» и тоже может породить синтаксическую ошибку. Получается, что шаблонная функция difference проверяет тип аргумента на одновременное выполнение сразу трёх условий: наличие difference_type , наличие оператора вычитания и возможность приведения результата вычитания к типу difference_type (преобразование подразумевается оператором return ). Но в то время, как типам, нарушающим первое условие, эта перегрузка не видна, нарушение второго или третьего условий вызовет ошибку компиляции.

Попробуем же придумать, как сделать метафункцию, которая говорит нам, есть ли в каком-то типе метод void foo(int) . Заботливая STL, особенно начиная с версии C++11, уже определила для нас много полезных метафункций, размещённых в основном в заголовках type_traits и limits , однако именно такой, какую мы дерзновенно замыслили сделать, там почему-то нет. Метафункция обычно выглядит как шаблонная структура без данных, внутри которой определён результат операции: заданный через typedef тип с именем type или статическая константа value . Такого соглашения придерживается STL, и причин оригинальничать у нас нет, поэтому будем придерживаться установленного образца.

Можно сразу написать «скелет» нашей будущей метафункции:
Она определяет наличие метода, из чего сразу ясно, что результат должен иметь булевский тип:
А вот теперь надо придумать, как сделать перегрузку, определяющую нужные нам свойства типа, и как получить из неё булевскую константу. Прелесть в том, что нам не нужно давать тела перегрузкам: поскольку вся работа происходит в режиме компиляции за счёт манипуляций с типами, хватит одних объявлений.
Очевидно, мы хотим, чтобы наша метафункция была применима для любого типа. Ведь про любой тип можно сказать, есть ли в нём искомый метод. Значит, has_foo не должна вызывать ошибки компиляции, какой бы параметр мы ни подставили. А ошибка-то произойдёт, если вдруг окажется, что нужного метода в типе T нет. Получается, что нам нужно две перегрузки одной проверочной функции. Одна из них, «детектор» должна быть синтаксически правильной только для типов, содержащих нужный метод. Другая, «подложка», должна быть всеядной, то есть быть достаточно подходящей для любых подставленных типов. В то же время «детектор» должен иметь неоспоримое преимущество в «подходящести» перед «подложкой». Наименее приоритетным и в то же время максимально всеядным в определении перегрузок является эллипсис (троеточие, обозначающее переменное количество аргументов):
Теперь надо объявить «детектор». Это должен быть шаблон: того, что он уже внутри шаблоной структуры, недостаточно! Нужен шаблон внутри шаблона [несколько секунд наслаждаемся одобрительными взглядами со стороны героев фильма Inception]. К «подложке» это не относится, поскольку её мы не будем выкидывать никогда. А вот для «детектора» воспользуемся волшебным словом decltype , которое определяет тип выражения, причём само выражение не вычисляется и в код не переводится. Подставим в качестве выражения вызов того самого метода, с аргументами нужного типа. Тогда ответом decltype будет возвращаемый тип метода. А если метода с таким именем нет, или он принимает другие типы аргументов, то мы получим ту самую контролируемую ошибку, которую и хотели. Пусть «детектор» возвращает то же, что и foo :

Если передадим в detect ссылку на const T& , получится, что U — тот же самый тип, что и T . Для проверки соответствия типа возвращаемого значения мы потом усовершенствуем детектор или придумаем что-то другое по ходу дела.
Однако постойте! Мы вызываем метод на свежесконструированном анонимном объекте, причём сконструирован-то он по умолчанию. А что будет, если мы передадим в has_foo тип, у которого нет конструктора по умолчанию? Конечно же, ошибка компиляции. Правильнее было бы объявить какую-нибудь функцию, возвращающую значение нужного типа. Вызываться она всё равно не будет, а нужный эффект будет достигнут. STL позаботилась и об этом: в заголовке utility есть функция declval :

Осталось только научиться отличать «подложку» от «детектора». Тут нам поможет всё тот же decltype . У «подложки» тип возвращаемого значения всегда void , а у «детектора» — тип, возвращаемый методом, то есть в случае, когда метод соответствует нашим требованиям… тот же самый void . Так не пойдёт. Сменим-ка мы для «подложки» тип на int . Тогда проверка получается простой: если вызов detect на объекте T имеет тип void , то сработал «детектор» и метод полностью соответствует нашим требованиям. Если тип другой, то либо сработала «подложка», либо метод существует, принимает те самые аргументы, но возвращает что-то не то. Проверяем, насколько заботлива STL, и тут же находим метафункцию проверки типов на равенство is_same :
Ура, мы добились желаемого. Как видите, всё и в самом деле достаточно просто. Отдадим дань уважения тем программистам, которые ухитрялись проделывать этот фокус в суровых условиях предыдущего стандарта, гораздо более многословно и хитроумно из-за отсутствия таких полезных штук, как declval .

SFINAE используется настолько широко, что даже в заботливую STL включили специальную метафункцию enable_if . Её параметры — булевская константа и тип (по умолчанию void ). Если передано true , то в метафункции присутствует тип type : тот, что передан вторым параметром. Если же передано false , то никакого type там нет, что и создаёт ту самую контролируемую ошибку. В свете соображений, перечисленных выше в аккуратном списочке, надо помнить, что enable_if сможет «вычеркнуть» перегрузку функции только если она — шаблон, а также озаботиться тем, чтобы список «невычеркнутых» перегрузок никогда не оставался совсем пустым. Можно применять enable_if и в специализациях шаблонного класса, но в таком случае это уже не SFINAE, а нечто вроде static_assert .

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

SFINAE

Это правило применяется во время разрешения перегрузки шаблонов функций: при сбое подстановки явно указанного или выведенного типа для параметра шаблона специализация отбрасывается из набора перегрузки, а не вызывает ошибку компиляции.

Эта функция используется при метапрограммировании шаблонов.

Explanation

Параметры шаблона функции подставляются (заменяются на аргументы шаблона)дважды:

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

Замена происходит в.

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

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

Только сбои в типах и выражениях в immediate context типа функции или ее типов параметров шаблона или ее явного спецификатора (начиная с C ++ 20) являются ошибками SFINAE. Если оценка замещенного типа / выражения вызывает побочный эффект, такой как создание некоторой специализации шаблона, генерация неявно определенной функции-члена и т. Д., Ошибки в этих побочных эффектах рассматриваются как серьезные ошибки. Лямбда — выражение не считается частью непосредственного контекста. (Так как C ++ 20).

Замена продолжается в лексическом порядке и останавливается при возникновении неудачи.

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

Type SFINAE

Следующими по типу ошибками являются ошибки SFINAE:

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

      Expression SFINAE

      Только константные выражения,которые используются в типах (таких как границы массивов),до C++11 должны были рассматриваться как SFINAE (а не как жесткие ошибки).

      Следующие ошибки выражения являются ошибками SFINAE.

      • Иллюстрированное выражение,используемое в типе параметра шаблона
      • Некорректное выражение,использованное в типе функции:

      SFINAE в частичных специализациях

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

      Примечания: в настоящее время частичная специализация SFINAE формально не поддерживается стандартом (см. также CWG issue 2054 ), однако LFTS требует, чтобы она работала, начиная с версии 2 (см. также идиому обнаружения ).

      Library support

      Стандартный компонент библиотеки std::enable_if позволяет создать ошибку замещения, чтобы включить или отключить определенные перегрузки на основе условия, оцененного во время компиляции.

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

      Компонент стандартной библиотеки std::void_t — еще одна служебная метафункция, упрощающая приложения SFINAE с частичной специализацией.

      Alternatives

      Там, где это применимо, диспетчеризация тегов , if constexpr (начиная с C++17), и концепции (начиная с C++20) обычно предпочтительнее использования SFINAE.

      static_assert обычно предпочтительнее SFINAE, если требуется только условная ошибка времени компиляции.

      Examples

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

      Defect reports

      Следующие отчеты о дефектах,изменяющих поведение,были применены ретроактивно к ранее опубликованным стандартам C++.

      SFINAE

      This rule applies during overload resolution of function templates: When substituting the explicitly specified or deduced type for the template parameter fails, the specialization is discarded from the overload set instead of causing a compile error.

      This feature is used in template metaprogramming.

      Contents

      [edit] Explanation

      Function template parameters are substituted (replaced by template arguments) twice:

      • explicitly specified template arguments are substituted before template argument deduction
      • deduced arguments and the arguments obtained from the defaults are substituted after template argument deduction

      Substitution occurs in

      • all types used in the function type (which includes return type and the types of all parameters)
      • all types used in the template parameter declarations
      • all expressions used in the function type
      • all expressions used in a template parameter declaration
      • all expressions used in the explicit specifier

      A substitution failure is any situation when the type or expression above would be ill-formed (with a required diagnostic), if written using the substituted arguments.

      Only the failures in the types and expressions in the immediate context of the function type or its template parameter types or its explicit specifier (since C++20) are SFINAE errors. If the evaluation of a substituted type/expression causes a side-effect such as instantiation of some template specialization, generation of an implicitly-defined member function, etc, errors in those side-effects are treated as hard errors. A lambda expression is not considered part of the immediate context. (since C++20)

      This section is incomplete
      Reason: mini-example where this matters

      Substitution proceeds in lexical order and stops when a failure is encountered.

      If there are multiple declarations with different lexical orders (e.g. a function template declared with trailing return type, to be substituted after a parameter, and redeclared with ordinary return type that would be substituted before the parameter), and that would cause template instantiations to occur in a different order or not at all, then the program is ill-formed; no diagnostic required.

      [edit] Type SFINAE

      The following type errors are SFINAE errors:

      • attempting to instantiate a pack expansion containing multiple packs of different lengths
      • attempting to create an array of void, array of reference, array of function, array of negative size, array of non-integral size, or array of size zero:
      • attempting to use a type on the left of a scope resolution operator :: and it is not a class or enumeration:
      • attempting to use a member of a type, where
      • the type does not contain the specified member
      • the specified member is not a type where a type is required
      • the specified member is not a template where a template is required
      • the specified member is not a non-type where a non-type is required
      • attempting to create a pointer to reference
      • attempting to create a reference to void
      • attempting to create pointer to member of T, where T is not a class type:
      • attempting to give an invalid type to a non-type template parameter:
      • attempting to perform an invalid conversion in
      • in a template argument expression
      • in an expression used in function declaration:
      • attempting to create a function type with a parameter of type void
      • attempting to create a function type which returns an array type or a function type

      [edit] Expression SFINAE

      Only constant expressions that are used in types (such as array bounds) were required to be treated as SFINAE (and not hard errors) before C++11.

      The following expression errors are SFINAE errors

      • Ill-formed expression used in a template parameter type
      • Ill-formed expression used in the function type:

      [edit] SFINAE in partial specializations

      Deduction and substitution also occur while determining whether a specialization of a class or variable (since C++14) template is generated by some partial specialization or the primary template. Compilers do not treat a substitution failure as a hard-error during such determination, but ignore the corresponding partial specialization declaration instead, as if in the overload resolution involving function templates.

      Notes: currently partial specialization SFINAE is not formally supported by the standard (see also CWG issue 2054), however, LFTS requires it works since version 2 (see also detection idiom).

      [edit] Library support

      The standard library component std::enable_if allows for creating a substitution failure in order to enable or disable particular overloads based on a condition evaluated at compile time.

      In addition, many type traits must be implemented with SFINAE if appropriate compiler extensions are unavailable.

      The standard library component std::void_t is another utility metafunction that simplifies partial specialization SFINAE applications.

      [edit] Alternatives

      Where applicable, tag dispatch , if constexpr (since C++17) , and concepts (since C++20) are usually preferred over use of SFINAE.

      static_assert is usually preferred over SFINAE if only a conditional compile time error is wanted.

      [edit] Examples

      A common idiom is to use expression SFINAE on the return type, where the expression uses the comma operator, whose left subexpression is the one that is being examined (cast to void to ensure the user-defined operator comma on the returned type is not selected), and the right subexpression has the type that the function is supposed to return.

Похожие статьи