Beyond the C++ Standard Library: An Introduction to Boost

Free download. Book file PDF easily for everyone and every device. You can download and read online Beyond the C++ Standard Library: An Introduction to Boost file PDF Book only if you are registered here. And also you can download or read online all Book PDF file that related with Beyond the C++ Standard Library: An Introduction to Boost book. Happy reading Beyond the C++ Standard Library: An Introduction to Boost Bookeveryone. Download file Free Book PDF Beyond the C++ Standard Library: An Introduction to Boost at Complete PDF Library. This Book have some digital formats such us :paperbook, ebook, kindle, epub, fb2 and another formats. Here is The CompletePDF Book Library. It's free to register here to get Book file PDF Beyond the C++ Standard Library: An Introduction to Boost Pocket Guide.

Function Objects and Higher-Order Programming. Generic Programming and Template Metaprogramming. Pearson offers special pricing when you package your text with other student resources. If you're interested in creating a cost-saving package for your students, contact your Pearson rep. He participates in the Boost newsgroups and is one of the Boost-Users moderators. We don't recognize your username or password. The work is protected by local and international copyright laws and is provided solely for the use of instructors in teaching their courses and assessing student learning.

You have successfully signed out and will be required to sign back in should you need to download more resources. An Introduction to Boost. If You're an Educator Preview this title online Additional order info. For instance, the rules for numeric conversions and type promotions are far from trivial. Other conversions are trivial, but tedious; how many times do we need to write a safe function[1] for converting between strings and ints, doubles and strings, and so on?

Conversions can be problematic in every library and program you write, and that's how and why the Conversion library can help. It provides facilities that prevent dangerous conversions and simplify recurring conversion tasks. They unambiguously state the programmer's intent. More important is to clearly convey our intents when writing it.

The reason for the exception is simple. Typically, if a failed conversion doesn't constitute a logical error, the pointer conversion is used, and if it is an error, the reference version is used. Unfortunately, the difference is quite subtleit boils down to an asterisk or an ampersandand it isn't always a natural choice.

What if a failed cast to a pointer type is an error? It always throws a std:: If that worries you, you can write a conversion function that throws an exception in case of failure. The function is parameterized on the type to convert to, and accepts one argument to be converted. There are two base classes, base1 and base2, and a class derived that inherits publicly from both of the base classes. If the extra speed is required, we must make sure that the downcasts are safe. The nature of the cast implies that you know it can't possibly fail, so there is no error handling, and no exception is ever thrown.

Your computer may melt. The Earth may stop spinning. You may float above the clouds. The only thing you can safely assume is that bad things will happen to your program. Optimizations on casts are likely indicators of a design problem. You can only find erroneous casts in testing, not production release builds , and if you've ever had to listen to a screaming customer on the other end of the phone, you know that catching errors in testing is rather important and makes life a lot easier. Even more likely is that you've been the customer from time to time, and know firsthand how annoying it is to find and report someone else's problems.

  • The Principles of Psychology (Volume 2 of 2)?
  • Beyond the C Standard Library: An Introduction to Boost!
  • Karlsson, Beyond the C++ Standard Library: An Introduction to Boost | Pearson!
  • Still Standing: The Untold Story of My Fight Against Gossip, Hate, and Political Attacks.
  • 3 editions of this work?
  • ?
  • Beyond the C++ Standard Library: An Introduction to Boost by Björn Karlsson;

For example, a long can typically hold a much greater range of values than a short, so what happens when assigning a long to a short and the long's value is outside of short's range? The answer is that the result is implementation defined a nice term for "you can never know for sure". Signed to unsigned conversions between same size integers are fine, so long as the signed value is positive, but what happens if the signed value is negative? It turns into a large unsigned value, which is indeed a problem if that was not the intention.

The rules are many and sometimes subtlethey can trap even the experienced programmer. Rather than stating all of the rules7 and then carry on, I'll give you examples of conversions that are subject to undefined or surprising behavior, and explain which rules the conversions adhere to.

When assigning to a variable from one of a different numeric type, a conversion occurs. This is perfectly safe when the destination type can hold any value that the source can, but is unsafe otherwise. For example, a char generally cannot hold the maximum value of an int, so when an assignment from int to char occurs, there is a good chance that the int value cannot be represented in the char. When the types differ in the range of values they can represent, we must make sure that the actual value to convert is in the valid range of the destination type.

Otherwise, we enter the land of implementation-defined behavior; that's what happens when a value outside of the range of possible values is assigned to a numeric type. The source type can be implicitly deduced from the function argument.


Beyond the C++ standard library : an introduction to Boost / Björn Karlsson - Details - Trove

Assignment from a Larger to a Smaller Type When assigning a value from a larger type for example, long to a smaller type for example, short , there is a chance that the value is too large or too small to be represented in the destination type. If this happens, the result is yes, you've guessed it implementation-defined.

We'll talk about the potential problems with unsigned types later; let's just start with the signed types. We convert strings to numeric values and vice versa. Many user-defined types can be converted to strings or created from strings. It is all too common to write the code for these conversions each time you need it, which suggests that it is very much suited for a reusable implementation.

Of course, there must be a conversion function somewhere to make it work, but conceptually, it can be thought of as a cast. The source type must be OutputStreamable and the destination type must be InputStreamable. In addition, both types need to be CopyConstructible, and the target also DefaultConstructible and Assignable. This is true for many types, including the built-in types and the string classes from the Standard Library. Whenever there's uncertainty that the Conversion Summary In this chapter, you have learned about the Boost. Again, code clarity was improved and we stayed clear of both undefined and implementation-defined behavior.

No more repetitive conversion functions. It is a tool that is very handy for converting different streamable data types. If you were to read the implementation for these casts, you'd agree that none of them are very complicated. Still, it took insight, vision, and knowledge to recognize the need for them and to implement them correctly, portably, and efficiently. Not many know the intricacies of integral type conversion and promotion. The Boost conversion "casts" include all of that knowledge and are well crafted and tested; they are excellent candidates for your use.

This is what Boost. Utility is, a collection of useful tools with no better home. They are useful enough to warrant inclusion in Boost, yet they are too small to deserve their own library. This chapter covers some of Boost. Utility's most fundamental and widely applicable tools. Then, we'll see what happens when you delete an object through a pointer to an incomplete typethat is, when the layout of the object being destroyed is unknown.

We'll also see how noncopyable prevents a class from ever being copied, which is arguably the most important topic of this chapter. It is an excellent way of testing preconditions, postconditions, and invariants. There are many variations for performing runtime assertions, but how do you assert at compile time? Of course, the only way to do that is to have the compiler generate an error, and while that is quite trivial I've inadvertently done it many thousand times , it's not obvious how to get meaningful information into the error message.

Furthermore, even if you find a way on one compiler, it's a lot harder to do it portably. It can be used at different scopes, as we shall see. For the first demonstration of its usage, we'll see how it is used at class scope. Consider a parameterized class that requires that the types with which it is instantiated are of integral type. We'd rather not provide specializations for all of those types, so what we need is to assert, at compile time, that whatever type our class is being parameterized on is indeed an integral type.

Now, we're going to get a little bit ahead of ourselves by using another Boost library for testing the typeBoost. They too can be useful, you know.

If You're a Student

The output depends on the compiler, but it is surprisingly consistent on most compilers. Suppose we tried to instantiate the class like this: For a template like this, the parameterizing type is an obvious example. You could also use assertions for other assumptions that the class makes, such as the size of certain types and such. There are hardly ever compiler warnings when delete-ing a pointer to an incomplete type, but it can cause all kinds of trouble, because the destructor may not be invoked.

This, in turn, means that cleanup code won't be performed. The functions accept one argument; the pointer or array to be deleted. Both of these functions require that the types they delete be known at the time they are destroyed that is, when they are passed to the functions. When utilizing the functions, simply call them where you would otherwise call delete. Your compiler will say something like this: In function 'void boost:: But when and how are incomplete types problems in our code?

The following section talks about exactly that. What's the Problem, Anyway? One example of its friendliness is the way that it automatically provides copy construction and assignment for our classes, should we decide not to do so ourselves. This can lead to some unpleasant surprises, if the class isn't meant to be copied or assigned to in the first place. When that's the case, we need to tell clients of this class explicitly that copy construction and assignment are prohibited.

I'm not talking about comments in the code, but about denying access to the copy constructor and copy assignment operator. Fortunately, the compiler-generated copy constructor and copy assignment operator are not usable when the class has bases or data members that aren't copyable or assignable. Usage To make use of boost:: Although public inheritance works, too, this is a bad practice. Public inheritance says IS-A denoting that the derived class also IS-A base to people reading the class declaration, but stating that a class IS-A noncopyable seems a bit far fetched.

The attempted copy construction of d2 fails because the copy constructor of noncopyable is private. The attempted assignment of d1 to d3 fails because the copy assignment operator of noncopyable is private. The compiler should give you something similar to the following output: It's clear that copying an assignment is prohibited when deriving from noncopyable. This can also be achieved by defining the copy constructor and copy assignment operator privatelylet's see how to do that.

By using some clever internal machinery, the template function addressof ensures that it gets to the actual object and its address. Usage To always be sure to get the real address of an object, use boost:: That said, here's a code-breaker for you: Probably not, because it cannot be made safe except when using local classes. For example, consider an overloaded function where one version is an ordinary function taking an int argument, and the other is a templated version that requires that the argument of type T has a nested type called type.

They might look like this: If the type of the argument is int, the first version is called. Assuming that the type is something other than int, the second templated version is called. This is fine, as long as that type has a nested type named type, but if it doesn't, this code does not compile. Is this really a problem? Well, consider what happens when another integral type is used, like short, or char, or unsigned long.

How could we have avoided this? We can do that. Utility Summary This chapter has demonstrated some useful utility classes that can greatly simplify our daily life. We talked about the base class noncopyable. By providing both a useful idiom and straightforward usage that catches the eye of anyone reading the code, it definitely deserves to be used regularly.

This is one of the shortest chapters in the book, and I suspect that you've read through it fairly quickly. It pays you back fast, too, if you start using these utilities right away. There are other utilities in Boost. Utility, which I haven't covered here. You might want to surf over to the Boost Web site and have a look at the online documentation to see what other handy tools there would suit you well in your current work. When you encounter a class with one operator from one of these sets, you typically expect to find the others, too. This is important for two reasons: A consistent naming scheme aids understanding; and these concepts, and the classes named after them, can be part of class interfaces, clearly documenting important behaviors.

When using the Standard Library containers and algorithms, one typically supplies at least some relational operators most commonly operator Operators Header: Each class contributes operators according to the concept it names. You use them by inheriting from themmultiply inheriting if you need the services of more than one.

Fortunately, there are some composite concepts defined in Operators obviating the need to multiply inherit for many common cases. The following synopses describe some of the most commonly used Operator classes, the concepts they represent, and the demands they place on classes derived from them. In some cases, the requirements for the actual concepts are not the same as the requirements for the concept base classes when using Operators. THRoughout the synopses, the concepts are always stated first, followed by the type requirements for classes deriving from them.

Rather than repeating all of the concepts in this library, I have selected a few important ones; you'll find the full reference at www. Note that the inheritance doesn't have to be public; private inheritance works just as well. So, clients of a class supporting the less than relation have good cause for expecting that the operators that must also at least implicitly be supported are also part of the class interface. Alas, if we just add the support for operator! With the use of the Operators library, this task is greatly simplified, and correctness and symmetry come almost for free.

In addition to the help that the library offers in defining the full sets of operators, the naming and definitions of the concepts that a class can support is made explicit in the definition of the class and by the Operators library! In this chapter, we have seen several examples of how using this library improves programming with operators by simplification and ensured correctness.

It is a sad fact that providing important relational and arithmetic operators for user-defined types is often overlooked, and part of the reason is that there is so much work involved to get it right. This is no longer the case, and Boost. Operators is the reason why. An important consideration when providing relational and arithmetic operators is to make sure that they are warranted in the first place. When there is an ordering relation between types, or for numeric types, this is always the case, but for other types of classes, operators may not convey intent clearly.

Operators are almost always syntactic sugar, and the importance of syntactic sugar must never be underestimated. Unfortunately, operators are also seductive. Use them wisely, for they wield vast power. When you choose to add operators to a class, the Boost. Operators library increases the quality and efficiency of your work. The conclusion is that you should augment your classes with operators only after careful thought, and use the Operators library whenever you get the chance! The Operators library is the result of contributions from several people. As is the case for most Boost libraries, innumerable other people have been involved in making this library what it is today.

For example, there are a number of validation tasks that are suitable for regular expressions. Consider an application that requires the input to consist only of numbers. Another program might require a specific format, such as three digits, followed by a character, then two more digits. You could validate ZIP Codes, credit card numbers, Social Security numbers, or just about anything else; and using regular expressions to do the validation is straightforward. Another typical area where regular expressions excel are text substitutionsthat is, replacing some text with other text.

Suppose you need to change the spelling of the word colour to color throughout a number of documents. Again, regular expressions provide the best means to do thatincluding remembering to make the changes also for Colour and COLOUR, and for the plural form colours, the verb colourize, and so forth. Yet another use case for regular expressions is in formatting of text. A marked subexpression is a part of the regular expression enclosed within parentheses. The text that matches a subexpression can be retrieved after calling one of the regular expression algorithms.

Examples of flags are icase, which means that the regular expression is ignoring case, and JavaScript, indicating that the syntax for the regex is the one used in JavaScript. Usage To begin using Boost. Regex is one of the two libraries the other one is Boost. Signals covered in this book that need to be separately compiled. You'll be glad to know that after you've built Boostthis is a one-liner from the command promptlinking is automatic for Windows-based compilers anyway , so you're relieved from the tedium of figuring out which lib file to use. This is one of the core classes in the library, and it's the one that stores the regular expression.

Creating one is simple; just pass a string to the constructor containing the regular expression you want to use. The first is the enclosing of a subexpression within parenthesesthis makes it possible to refer to that subexpression later on in the same regular expression or to extract the text that matches it.

We'll talk about this in detail later on, so don't worry if you don't yet see how that's useful. The second feature is the wildcard character, the dot. The wildcard has a very special meaning in regular expressions; it matches any character. This regular expression is ready to be used in one of the algorithms, like so: The result of calling the function is true if there is an exact match for the regular expression; otherwise, it is false.

Do you see why that's not the case for this code? Look again at the regular expression. The first character is a capital A, so that's obviously the first character that could ever match the expression. So, a part of the input"A and beyond. Let's try another input string. When the regular expression engine matches the A, it then goes on to see what should follow. In our regex, A is followed by the wildcard, to which we have applied the Kleene star, meaning that any character is matching any number of times.

Thus, the parsing starts to consume the rest of the input string, and matches all the rest of the input. Validating Input A common scenario where regular expressions are used is in validating the format of input data. Applications often require that input adhere to a certain structure. Let's assemble a regular expression that can validate such input correctly. First, we need an expression that matches exactly 3 digits.

To have it repeated 3 times, there's a special kind of repeat called the bounds operator, which encloses the bounds in curly braces. Putting these two together, here's the first part of our regular expression. For text-processing validation tasks, regular expressions are much more scalable and reliable than handcrafted parsers. For searching and replacing, there are a number of problems that are very elegantly solved using regular expressions, but virtually impossible to solve without them.

Regex is a powerful library so it has not been possible to cover all of it in this chapter. Similarly, the great expressiveness and range of application of regular expressions necessarily means that this chapter offers little more than an introduction to them. These topics could easily fill a separate book.

  1. The Cornea in Normal Condition and in Groenouw’s Macular Dystrophy?
  2. Fire and Ice (The Chronicles of Light and Darkness Book 3).
  3. Refine your editions:?
  4. Ian Rush - An Autobiography With Ken Gorman.
  5. If You're an Educator.
  6. To learn more, study the online documentation for Boost. Regex and pick up a book on regular expressions consult the Bibliography for suggestions. Despite the power of Boost. Regex, and the breadth and depth of regular expressions, even complete neophytes can use regular expressions effectively with this library. It's easy to use and fast as lightning when matching your regular expressions.

    Use it as often as you can. The author of Boost. Containers and Data Structures This part of the book covers the libraries Boost. They are all containers in some sense, although they have virtually nothing in common with the Standard Library container types. These are all extremely useful libraries, which many others and I use to solve programming problems most every day. It's interesting to ponder how much the availability of basic data structures affect how we program, and even how we design.

    • Beyond the C++ Standard Library: An Introduction to Boost - Björn Karlsson - Google Книги.
    • Whisper Not Thy Name;
    • The Coherence of Kants Transcendental Idealism: 4 (Studies in German Idealism).
    • The Scottish Huntsupe!

    Without existing structures, we craft our own, and typically do so with significant consideration for the solution domain, which limits the reusability of our work. That's a common theme for all types of programming, of course, and the tradeoff is between genericity and basically just getting the job done. The value of flexible libraries that addresses both the issues we have at hand, and most issues we are likely to encounter at a later time, is substantial.

    It is like a variant type on steroids: It will hold any type, but you have to know the type to retrieve the value. There are times when you need to store unrelated types in the same container. There are times when certain code only cares about conveying data from one point to another without caring about the data's type. At face value, it is easy to do those things. They can be done using a discriminated union.

    There are numerous variant types available that rely on some type tag mechanism. Unfortunately, all of these suffer from a lack of type safety, and only in the most controlled situations should we ever purposely defeat the type system. The Standard Library containers are parameterized on the type they contain, which poses a seemingly impossible challenge for storing elements of heterogeneous types in them.

    There is no way to get to the contained value without knowing its exact type, and thus, type safety is preserved. When designing frameworks, it isn't possible to know in advance about the types that will be used together with the framework classes. A common approach is to require the clients of the framework to adapt a certain interface, or inherit from base classes provided by the framework.

    This is reasonable, because the framework probably needs to communicate with various higher-level classes in order to be useful. There are, however, situations where the framework stores or otherwise accepts types that it doesn't need to or can know anything about. One important property of Any is that it provides the capability to store objects of heterogeneous types in Standard Library containers. Unlike indiscriminate types, any preserves the type, and actually does not let you near the stored value without knowing the correct type.

    Of course, there are means for querying for the type, and testing alternatives for the contained value, but in the end, the caller must know the exact type of the value in an any object, or any denies access. Think of any as a locked safe. Without the proper key, you cannot get in. This is the public interface of any: Of course, there is no way of retrieving the value of an empty any, because no value exists. The value that is contained in other is copied and stored in this.

    The creation of an instance capable of storing any conceivable value is straightforward. However, to actually do anything with the value contained in an any, we need to retrieve it, right? For that, we need to know the value's type. The following, however, does work. These two elements are all you need to remember, typewise, for this library: Consider three classes, A, B, and C, with no common base class, that we'd like to store in a std:: Well, not any more pun intended , because the type of any does not change depending on the type of the value it contains. The following code shows how to solve the problem.

    This is for good reasons: Type safety keeps us from making mistakes and improves the performance of our code. So, we avoid indiscriminate types. Still, it is not uncommon to find oneself in need of heterogeneous storage, or to insulate clients from the details of types, or to gain the utmost flexibility at lower levels of a hierarchy. This design can be used to create generic function objects, generic iterators, and much more. It is an example of the power of encapsulation and polymorphism in conjunction with templates.

    In the Standard Library, there are excellent tools for storing collections of elements. When the need for storage of heterogeneous types arises, we want to avoid having to use new collection types. In a way, the template class any extends the capabilities of the Standard Library containers by packaging disparate types in a homogeneous wrapper that allows them to be made elements of those aforementioned containers.

    Any to an existing code base is straightforward. It doesn't require changes to the design, and immediately increases flexibility where it's applied. The interface is small, making it a tool that is easily understood. The Any library was created by Kevlin Henney, and like all Boost libraries, has been reviewed, influenced, and refined by the Boost community. Variant library has many features in common with Boost. Any, but there are different tradeoffs as well as differences in functionality. The need for discriminated unions variant types is very common in everyday programming.

    One typical solution while retaining type safety is to use abstract base classes, but that's not always possible; even when it is, the cost of heap allocation and virtual functions[1] may be too high. The library we look at hereBoost. Variantsupports bounded variant typesthat is, variants where the elements come from a set of supported types. Variant types are available in many other programming languages, and they have proven their worth time and again. Variant remedies the situation through a class template variant, and accompanying tools for safely storing and retrieving values.

    A variant data type exposes an interface independent of the current value's type. If you've used some proprietary variant types before, you may have been exposed to types that only support a fixed set of types. That is not the case with this library; you define the set of types that are allowed in a variant when you use it, and a program can contain any number of disparate variant instantiations. To retrieve the value that is held in a variant, you either need to know the exact type of the current value, or use the provided typesafe visitor mechanism.

    The visitor mechanism makes Variant quite different from most other variant libraries, including Boost. Any which on the other hand can hold a value of any conceivable type , and thereby enables a safe and robust environment for handling such types. Finally, efficiency aspects are covered, too, as the library stores its values in stack-based storage, thus avoiding more expensive heap allocations.

    Variant permits storing heterogeneous types in the Standard Library containers. The following partial synopsis covers the most important members of the variant class template. Other functionality, such as the visitation mechanism, direct typesafe value retrieval, and advanced features such as creating the set of types through type sequences, are described in the "Usage" section. This header includes the entire library, so you don't need to know which individual features to use; later, you may want to reduce the dependencies by only including the relevant files for the problem at hand.

    When declaring a variant type, we must define the set of types that it will be capable of storing. The most common way to accomplish this is using template arguments. A variant that is capable of holding a value of type int, std:: We can also pass a value that is convertible to one of those types to initialize the variant. If we want to see that this is the case, we can retrieve the value using the function boost:: To avoid getting an exception upon failure, we can pass a pointer to a variant to get, in which case get returns a pointer to the value or, if the requested type doesn't match the type of the value in the variant, it returns the null pointer.

    Beyond the C++ Standard Library: An Introduction to Boost

    Here's how it is used: Note that the type must match exactly, including at least the same cv-qualification const and volatile. However, a more restrictive cv-qualification will succeed. If the type doesn't match and a variant pointer is passed to get, the null pointer is returned. Variant library does an excellent job of providing efficient and easy-to-use variant types based upon discriminated unions. Many attempts at creating discriminated unions have suffered from significant drawbacks.

    For example, previous attempts usually come with a fixed set of supported types, which seriously impedes maintainability and flexibility. Variant avoids this limitation through templates, which theoretically allows creating any variant type. Type-switching code has always been a problem when dealing with discriminated unions; it was necessary to test for the type of the current value before acting, creating maintenance headaches. Variant offers straightforward value extraction and typesafe visitation, which is a novel approach that elegantly solves that problem.

    Finally, efficiency has often been a concern with previous attempts, but this library addresses that too, by using stack-based storage rather than the heap. Variant is a mature library, with a rich set of features that makes it easy and efficient to work with variant types. It nicely complements the Boost. The authors of Boost.

    See a Problem?

    Variant are Eric Friedman and Itay Maman. However, that one value can be of arbitrary type, which allows grouping multiple values as the result, with a struct or class. Although possible, it is often inconvenient to group related return values in such constructs, because it means defining types for every distinct return type needed. To avoid copying large objects in a return value, and to avoid creating a special type to return multiple values from a function, we often resort to using non-const reference arguments or pointers, thereby allowing a function to set the caller's variables through those arguments.

    This works well in many cases, but some find the output parameters disconcerting in use. Also, output parameters don't emphasize that the return value is in fact return values. To provide for multiple return values, we need a tuple construct. A tuple is a fixed-size collection of values of specified types.

    Examples include pairs, triples, quadruples, and so on. The Tuple library provides tuple constructs that are convenient to use for returning multiple values but also to group any types and operate on them with generic code. All I can say, therefore, is that this book will introduce you to Boost and that it will do it well. Aug 24, Tom Panning rated it really liked it. A good introduction to the parts of Boost that are probably the most useful to the most programmers.

    What's particularly nice is that first the author shows you the problem, and how you might solve it without Boost, before showing you how Boost solves it and the advantages. Of course, if you're not comfortable with templates you may have trouble following along.

    Sep 19, Rembo rated it it was amazing. It does not cover all of boost, but the topics it covers are very relevant and give you a good insight on what's in boost and the general usage patterns. Geoffrey Burns rated it liked it Jul 24, Rajesh rated it liked it Sep 14, Paul Butcher rated it really liked it Jun 21, Vincent Jacques rated it really liked it Mar 16, Austin Gilbert rated it it was amazing Oct 23, Lorenzo Caminiti rated it did not like it Mar 19, Altan Alpay rated it did not like it Mar 07, Nevin rated it it was amazing Jan 30, Mlimber rated it liked it Jun 14, Dan Delgado rated it it was ok Jan 01, Norman rated it it was amazing Jan 12, Craig rated it liked it May 07, Mike Stack rated it did not like it Aug 06, Lorenzo De Leon rated it really liked it Nov 25, David rated it really liked it Jun 02, Dreadful rated it really liked it Feb 14, Florin rated it really liked it May 31,