Generic <programming>: mapping between types and values

zhaozj2021-02-16  50

Generic : mapping between type and values ​​Andrei Alexandrescu

In C , the term "conversion" describes the process of acquiring a type (type) value from another type of value (value). But sometimes you need a different type of transformation: it may be a value you need to get a value when you have a type, or other similar situations. This transformation in C is unusual because the type domain and value domain have a very stringent boundary. However, in some specific occasions, you need to cross these two borders, this column is to discuss how to do this.

Mapping integer is type

A simple template for many generic programming programming styles:

Template struct int2type {enum {value = v};

Each of the different constant values ​​transmitted, INT2TYPE "generates" a different type. This is because the entity of different templates is different types, so INT2TYPE <0> is different from other types such as INT2TYPE <1>. In addition, the value of the type of type is "stored" in the member value of enumeration (enum).

Regardless of any time, you can use int2type when you need a constant constant to quickly "Typify". For example, you want to design a NiftyContainer class template.

Template Class NiftyContainer {...};

NiftyContainer stores pointers pointing to T. In some member functions of NiftyContainer, you need to clone type T object if t is a non-multi-state type, you may say this:

T * psomeObj = ...; t * pnewobj = new t (* psomeobj);

For T is the case of a polymorphic type, the situation is more complicated, then we assume that you have established this rule, all of which use the multi-state type of NiftyContainer must define a Clone virtual function (Virtual function). Then you can clone the object like this:

T * pnewobj = psomeobj-> clone ();

Because your container must be able to accept these two types, you must implement two clongegas and choose the appropriate one at compilation time. Then, regardless of the type passing through the NiftyContainer's Boolean parameter, you have to interact with it, but also rely on programmers to deliver the correct logo.

Template Class NiftyContainer {...};

NiftyContainer widgetbag; NiftyContainer Numberbag;

If you store the type in the NiftyContainer is not a polymorphic, you can optimize the many member functions of the NiftyContainer because you can use the constant object size and value semantics. In all of these member functions, you need to select an algorithm or another algorithm that depends on template parameter ispolymorphic. At first glance, it seems that only one IF statement can be used.

Template Class NiftyContainer {... Void Dosomething (T * Pobj) {if (ispolymorphic) {... Polymorphic Algorithm ...} else {... non-polymorphic algorithm ...}} }

The problem is that the compiler will not let you get rid of these codes. For example, if the multi-state algorithm uses POBJ-> Clone, then NiftyContainer :: DOSMETHING will not be compiled with any of the types that do not define the CLONE member function. Indeed, it seems that it is obvious that it is obvious to perform which if statement branch, but this does not criticize the compiler, the compiler still insists on compiling these two branches, even if the optimizer will eventually eliminate these DEAD CODE. If you try to call the DOSMETHING function of NiftyContainer , the compiler will stay in the call of Pobj-> Clone, what is going on?

Wait, the problem is more. If t is a polymorphic type, the code will not be compiled again once. If t is set to Private and Protected to private and protected, external to its access - as a good polymorphism class, it should be. So, if the non-multi-state code branch is to do new t (* pobj), the code cannot be compiled.

If the compiler does not compile the abandonment code, it is good, but the expectation of hope is not a solution, so how is it a satisfactory solution?

There are many solutions that have been confirmed. INT2TYPE provides a very delicate solution. The value corresponding to the ISPOLYMORPHIC is TRUE and FALSE, and INT2TYPE can convert a specific Boolean ISPOLYMORPHIC into two different types. Then you can use int2type through simple overloading, get it!

"Integral Typifying" style prototype (incarnation) as follows:

template class NiftyContainer {private: void DoSomething (T * pObj, Int2Type ) {... polymorphic algorithm ...} void DoSomething (T * pObj, Int2Type ) {.. . Non-polymorphic algorithm ...} public: void Dosomething (t * pobj) {dosomething (pobj, int2type ());}}; this code is simple, DOSMETHING calls overloaded private member functions, according to according to according to ISPOLYMORPHIC's value, one of the two private overload functions is called, thereby completing a branch. Here, the virtual temporary variable of type INT2TYPE is not used, it is just for the transfer of type information.

Don't be too fast, the sky!

Seeing the above way, you may think there is a more clever solution, you can use tips such as Template Specialization. Why do you have to use virtual temporary variables, there must be a better way. But amazing is that int2type is difficult to defeat in simplicity, versatility and efficiency.

One possible attempt is made to NiftyContainer :: Dosomething as a special process based on two possible values ​​of any T and ISPOLYMORPHIC. Isn't this a man play of Partial Template Specializance?

Template Void NiftyContainer :: DOSMETHING (T * Pobj) {... polymorphic algorithm ...}

Template Void NiftyContainer :: DOSMETHING (T * Pobj) {... non-polymorphic algorithm ...}

It looks beautiful, but, it's not good, it is illegal. There is no way to make Partial Specialization for a member function of a class template, you can make Partial Specialization for the entire NiftyContainer:

Template Class NiftyContainer {... Non-Polymorphic NiftyContainer ...};

You can also make Specialization for the entire Dosomething:

Template <> void NiftyContainer :: dosomething (int * pobj) {... non-polymorphic algorithm ...}

But strangely, between [1], you can't do anything.

Another way may be to introduce Traits Technology [2], and implement DOSMETHING outside the NiftyContainer, but the DOSMETHING is separated. It is a bit awkward.

The third approach is still trying to use Traits technology, but puts the implementation together, which is to define Traits as private internal classes in NiftyContainer. In short, this is ok, but when you try to achieve it, you will recognize how good INT2TYPE-based style is. And the best place in this style is that in practical applications, you can put this small int2type template in the library and use its expectations to record. Type to type mapping

Consider the following functions:

Template T * Create (const u & arg) {Return New T (arg);

CREATE creates a new object by transmitting a configuration function (constructor) to T.

Now suppose in your application: Type Widget's object is a left code. You must bring two parameters when constructive, and the second parameter is a fixed value like -1. In all derived from the widget class, you won't encounter something.

How do you want to make specialization for Create to make it different from all other types when processed Widget? You can't make partial specialization for functions, that is, you can't do this:

// illegal code - don't try this at homeemplate widget * create (const u & arg) {Return New Widget (arg, -1);}

Due to the lack of a function of Partial Specialization, the only tool we have, or overloaded. You can pass a virtual object of Type T and overload.

// An Implementation of Create Relying On OverLoadingTemplate T * Create (const U & arg, t) {Return New T (arg);} template widget * create (const u & arg, widget) {Return New Widget (arg, -1);} // use create () string * pstr = create ("hello", string ()); widget * pw = create (100, widget ());

The second parameter of CREATE is only for the use of the appropriate overload function. This is also a problem with this kind of homemade style: You waste time on the complex object that you don't use, even Can help you, but if the widget blocks the default construtor, the optimizer also loves it.

The Proverbial Extra Level of Indirection Can Help Here, TOO. (Sorry, this sentence doesn't know how to translate) an idea is: transfer T * rather than t comes as a virtual template parameter. At runtime, you can always pass empty pointers, which is quite low in constructive.

Template t * create (const u & arg, t *) {return new t (arg);} template widget * crete (const u & arg, widget *) {return new widget (Arg , -1);} // use create () string * pstr = crete ("Hello", (String *) 0); widget * pw = create (100, (widget *) 0); this way for Create The user is the most confusing. In order to keep this solution, we can use similar templates with INT2TYPE.

Template struct type2type {type;

Now you can write this:

Template T * Create (const u & arg, type2type ) {Return New T (Arg);} template widget * create (const u & arg, type2type ) {Return New widget (arg, -1);} // use create () string * pstr = crete ("Hello", type2type ()); widget * pw = create (100, type2type ());

Of course, this is certainly more explanation than other solutions. Of course, you have to contain Type2Type inside the library and will be recorded in the case where it is expected.

Check convertibility and inheritance

When implementing template functions and template classes, you often encounter such problems: give two strong types B and D, how do you check that D is it derived from B?

This relationship is found in compilation moments when making a general library for further optimization. In a universal function, if a class implements a specific interface, you can use an optimized algorithm without having to make a Dynamic_CAST.

Check that the derived relationship is a more general mechanism, that is, check the translatable. At the same time, we have to solve such a more common problem: How do you find a strong type T whether to support automatic conversion into a strong type U?

This issue has a solution that can be used by SIZEOF. (You may think, SIZEOF is not using parameters for MEMSET, is it?) SizeOf has a very much purpose, because you can use SizeOf to any expression (Expression), at runtime, no matter how much Complex, do not need to be more than the value of the expression, the size of the SIZEOF always returns the size of the expression. This means that SizeOf knows the rules of heavy duty, template physical and transformation - each participating in C expression. In fact, SIZEOF is a feature-free tool for deriving expression types. Finally, SIZEOF has an expression and returns the size of the expression results [3].

The idea of ​​convertible inspections uses SIZEOF with the overloaded function. You can subtly provide two overloads for a function: A type of acceptance can be converted to u, and the other can accept any type. This way you can call the overload function of t as the parameter, where t is the type you want to convert to u. If the function of the incoming parameter is called, then t will be converted to u; if the "Fallback" function is called, T does not translate into U. In order to check which function is called, you can let the two overload functions return different sizes, and identify them with SizeOf. As long as it is the type of different sizes, they are difficult to escape.

First create two types of different sizes (obviously, char and long double have different sizes, but the standard does not guarantee this guarantee), a simple code summary as follows:

Typedef char small; struct big {char dummy [2];

As can be seen from the definition, the size of the size is 1, and the size of the BIG is unknown, but it is definitely greater than 1. For us, it can guarantee that this is enough.

Next, you need to do two overloads, one accept U and returns a small.

Small test (u);

Then how do you want to write a function that can accept any "Dongdong"? Templates can not solve this problem because the template will look for one of the most match, which hides transformation. What we need is a match that is more poor than automatic conversion. Quickly browse the transformation rules applied to a function call for the omitted number. It is the end of the list, that is the worst one, this is precisely what we need.

BIG TEST (...);

(Calling a function of a c object generates an unknown result, who cares about it? Do not have anyone really calling such a function, this function will not even be implemented.)

Now I want to use SIZEOF to Test's call, pass it to it:

Const Bool Convexists = Sizeof (TEST (TEST (TEST (TEST)) == SIZEOF (SMALL);

that's it! Test call generates a default constructor T, then SizeOf extracts the size of the result of the expression. It may be sizeof (small), or may be SIZEOF (BIG), which is to see if the compiler finds the possibility of transformation.

There is also a small problem. What is the private member if t puts its default constructor? In this case, T 's expression will not be compiled, all of which we build is white. Fortunately, this is another simple solution - just use a function like a scarecrow that can return T like a scarecrow. In this way, all problems are solved!

T Maket (); const bool convexists = sizeof (TEST ())) == SIZEOF (SMALL);

(By The Way, ISN't It Nifty Just How Much you can do with west, what, which not only don't do anything, but Which don't even real exist at all?) (By the way, Just like Maket and Test, this kind of function is good, depending on what you use it, it not only doesn't do anything, but doesn't even exist at all?) Now we can make it work, put everything In a class template, hiding all the details about type derivation, only exposing the results.

Template Class Conversion {Typedef Char Small; Struct Big {Char Dummy [2];}; static small test (u); static big test (...); t maket (); public: ENUM {EXISTS = SIZEOF (TEST (Maket ())) == SIZEOF (SMALL)};

Now you can test the Conversion template class like this.

INT main () {using namespace std; cout << Conversion :: exists << '' << Conversion :: exists << '<< Conversion > :: EXISTS << ';}

The print result of this applet is "1 0 0". We noticed that although the std :: Vector implemented a constructor with parameters as size_t, the result of the conversion test returned or 0 because the constructor is explicit (Explicit).

We can implement such two or more constants in Conversion.

EXISTS2WAY indicates whether it can be transformed with each other between T and u. For example, INT and DOUBLE are the cases that can be transformed, but various custom types can achieve such mutual conversion. Sametype indicates whether T and u are the same type.

Template Class Conversion {... as Above ... enum {exists2way = exists && conversion :: exists}; enum {sametype = false};

We implement SameType by making Partial Specialization for Conversion.

Template Class Conversion {public: enum {exists = 1, exissrs2way = 1, Sametype = 1};

So, how can I do a derived relationship? The most beautiful place is here, as long as you handle transformation, the test relationship is simple.

#define supersubclass (b, d) / (conversion :: exists&&! conversion :: Sametype) Is it clear? There may be a little confused. SuperSubClass (B, D) determines whether D is true from B is true, or B and D are the same type. SuPERSUBCLASS can make such a judgment by discriminating the conversion of const D * to Const B *. Const D * implicit transformation to const b * only three cases:

B and D are the same type; B is a clear public base class of D; B is an empty type. The last case can be excluded by the second test. In practical applications, the first case (B and D is the same type) test is useful. Because you usually consider a class is its own superclass because of the actual consideration. If you need a stricter test, you can write this:

#define supersubclass_strict (b, d) / (supersubclass (b, d) && /! conversion :: Sametype)

Why add const modifications in these codes? The reason is that you never want to make the conversion test fail on due to const. So, use const in each place, if the template code uses two const (using consts for a type already const), the second const will be ignored. In short, use const in supersubclass is based on safe consideration.

Why SUPERSUBCLASS and not the cuter BASE_OF or INHERITS For a very practical reason: with INHERITS (B, D), I kept forgetting which way the test works - does it test whether B inherits D or vice versa SUPERSUBCLASS (B, D)?? Makes It Clearer (at Least for Me) Which One is first and which one is second. Why is SuPERSUBCLASS instead of more appropriate base_of or inherits? For an actual reason: use inherits (b, d), I will often forget the way to detect - it is testing B derived from D, or D derived from B? For this issue (who is the first one who is the second), SuperSubclass (B, D) shows more clearly (at least for me).

summary

Introduce these three styles here, one of the most important places is that they are reusable. You can write them in a library and allow programmers to use them, rather than they have to master this complex internal implementation.

The reusability of Nontrivial technology is very important. People remember a complex technology, even if this technology can be used to help their actual work more simplified, but if this technology is slightly trouble, they will not use it. . Give people a simple black box, it can bring some useful surprises, people like it and use it, because this is a "freedom" way.

INT2TYPE, TYPE2TYPE, especially Conversion belongs to a universal tool library. By deriving the types of important compilation moments, they extend the ability of programmers' compilation moments. Thank you

If Clint Eastwood asked me: "Do you feel lucky?", My answer is definitely "yes". This is the third article of my series, which is beneficial to the direct attention and important recommendations of Herb Sutter. Thanks to Tomio Hoshida of Japan, Tomio Hoshida, found a bug and made some advice with profound insight.

Note: This article refers to a book from Andrei Alexandrescu that is scheduled to be "Modern C Design" (Addison-Wesley, 2001). (Translation: This book has been published, if you can read it soon, how much it is more cool) Note [1] C to function's Partial Specialization support is no conceptual obstacle, this is a very valuable feature. [2] Andrei Alexandrescu. "Traits: The else-if-type", C Report (April 2000). [3] It is recommended to add a type of operator in C , which is a type of return to an expression. Operators. With such an operator, it will make the writing template code easier to write, easy to understand. GNU C has implemented TypeOf as an extension. Obviously, TypeOf and SizeOF have the same obstacle, because, in any case, SIZEO must calculate the type. [See Bill Gibbons in a article on CUJ, he introduced a beautiful method, which is used to create a (almost) natural TypeOf operator in the current Standard C . ]

转载请注明原文地址:https://www.9cbs.com/read-28002.html

New Post(0)