Típusinformáció: Sztring == sZtRIng
Adott az alábbi sztring osztály, amely most az egyszerűség kedvéért nem használ dinamikus memóriakezelést:
class String {
private:
char data[100];
public:
String() {
data[0] = 0;
}
String(char const *init) {
strcpy(data, init);
}
bool operator==(String const &rhs) {
String const &lhs = *this;
size_t i;
for (i = 0; lhs.data[i] != 0 && rhs.data[i] != 0; ++i)
if (lhs.data[i] != rhs.data[i])
return false;
return lhs.data[i] == 0 && rhs.data[i] == 0;
}
};
int main() {
String s1 = "hello", s2 = "HeLLo";
std::cout << (s1 == s2);
}
Ezen a sztring osztályon több dolgot is lehetne általánosítani. Például a karakter típusa az egybájtos char
-on
túl lehetne másféle is, pl. char16_t
vagy char32_t
az Unicode kódolású szövegek tárolásához. Továbbá,
az összehasonlítást nem feltétlenül kellene az ==
operátorral végezni. Lehetne pl. úgy is, hogy figyelmen kívül
hagyjuk a kisbetű–nagybetű különbségeket.
Ezek sablonparaméterek lehetnének. Azért, hogy ne legyen túl sok sablonparamétere a String
osztálynak, érdemes
ezeket egy külön osztályba tenni. Például a fenti viselkedés leírásához a CharTraits
osztályban kell lennie egy
belső típusnak, és egy statikus függvénynek, CharTraits::type
és CharTraits::equal()
:
struct CharTraits {
using Type = char;
static bool equal(char c1, char c2) { return c1 == c2; }
};
- Írd át úgy a fenti
String
osztályt, hogy a sablonparamétere egy ilyen viselkedésosztály legyen! Használják az osztály függvényei a sablonparaméterből vett típust és karakterösszehasonlító függvényt! Figyelj arra, hogy ehhez astrcpy()
függvényhívást is át kell írnod, mert az csakchar*
-okon működik. - Példányosítsd az új
String
sablonod a fentiCharTraits
osztállyal, és teszteld a működését! - Hozz létre egy olyan karakterosztályt is, amelynek karakterei érzéketlenek a kisbetű–nagybetű különbségre! Teszteld ezt is!
- Mi történik, ha összehasonlítasz az
==
operátorral két sztringet, amelyek karakterosztálya eltérő? Miért?
Szorgalmi feladat: Nézz utána a szabványos std::basic_string
, std::basic_ostream
, és az std::char_traits
osztályoknak, és hasonlítsd össze őket a most megírt programoddal!
Ez a feladat a GotW #29 alapján készült.
Megoldás
#include <iostream>
template <typename CHAR_TRAITS>
class String {
private:
using CharType = typename CHAR_TRAITS::Type;
CharType data[100];
public:
String() {
data[0] = 0;
}
String(CharType const *init) {
size_t i;
for (i = 0; init[i] != 0; ++i)
data[i] = init[i];
data[i] = 0;
}
bool operator==(String const &rhs) {
String const &lhs = *this;
size_t i;
for (i = 0; lhs.data[i] != 0 && rhs.data[i] != 0; ++i)
if (!CHAR_TRAITS::equal(lhs.data[i], rhs.data[i]))
return false;
return lhs.data[i] == 0 && rhs.data[i] == 0;
}
};
struct CharTraits {
using Type = char;
static bool equal(char c1, char c2) {
return c1 == c2;
}
};
struct CharTraitsCaseInsensitive: public CharTraits {
static bool equal(char c1, char c2) {
return toupper(c1) == toupper(c2);
}
};
int main() {
String<CharTraits> s1 = "hello", s2 = "hEllo";
std::cout << (s1 == s2) << std::endl;
String<CharTraitsCaseInsensitive> s3 = "hello", s4 = "hEllo";
std::cout << (s3 == s4) << std::endl;
}
A CharTraitsCaseInsensitive
osztály származtatható a CharTraits
osztályból, így megörökli
a Type
belső típust. Ennek a származtatásnak akkor jönne ki igazán az előnye, ha több belső függvény lenne, mert
csak egyet szeretnénk felülírni. Bár az equal()
függvény statikus, és így nem lehet virtuális, mégis felüldefiniálható
a leszármazásban, mert a sablonoknál fordítási időben a megfelelő típus látszik!
Az eltérő viselkedésosztályból példányosított sztring objektumok nem hasonlíthatók össze, mert különböző típusúnak számítanak. Ez az összehasonlításnál nem is baj, amúgy sem lehet eldönteni, hogy egy kisbetű–nagybetűre érzéketlen és egy arra érzékeny sztringet hogyan kellene összehasonlítani.
Az std::basic_string
és az std::char_traits
ugyanígy működnek; std::string
==
std::basic_string<char, std::char_traits<char>>
, illetve std::char_traits<char>::char_type
és std::char_traits<char>::eq()
.
print()
A feladat egy olyat print() függvénycsaládot írni, amelyik meg tudja különböztetni, más formátumban írja ki a számokat, a karaktereket és a sztringeket.
- A számokat (int, double, egyebek) önmagukban, pl. 5 vagy 5.1.
- A karaktereket (pl. char vagy char16_t) aposztrófok között: 'a'.
- A sztringeket (pl. char* vagy std::string) pedig idézőjelek között: "hello".
- Egyéb típusokat ne lehessen kiírni vele.
A feladatot sablon metaprogramozással oldd meg!
Útmutatás. Elvileg csak három print() függvényre lesz szükséged, amelyek közül mindig valamelyiket az std::enable_if engedélyezi, a többit tiltja. Nézz szét a type_traits dokumentációjában, milyen sablonokat tudsz felhasználni és miket kell magadnak megírnod. Figyelned kell arra is, hogy T és T const eltérő típusok, ugyanakkor vissza lehet vezetni egyiket a másikra. És arra, hogy ne zsúfolj mindent a print()-ek fejlécébe; ha kell, írj segédfüggvényeket (metafüggvényeket)!
Megoldás
#include <iostream>
#include <type_traits>
#include <string>
template <typename T> struct my_is_char { static constexpr bool value = false; };
template <> struct my_is_char<char> { static constexpr bool value = true; };
template <> struct my_is_char<signed char> { static constexpr bool value = true; };
template <> struct my_is_char<unsigned char> { static constexpr bool value = true; };
template <> struct my_is_char<char16_t> { static constexpr bool value = true; };
template <> struct my_is_char<char32_t> { static constexpr bool value = true; };
template <> struct my_is_char<wchar_t> { static constexpr bool value = true; };
template <typename T> struct my_is_string { static constexpr bool value = false; };
template <typename T> struct my_is_string<std::basic_string<T>> { static constexpr bool value = true; };
template <typename T> struct my_is_string<T*> { static constexpr bool value = my_is_char<typename std::remove_cv<T>::type>::value; };
template <typename T>
void print(T what, typename std::enable_if<std::is_arithmetic<T>::value && !my_is_char<T>::value>::type * = nullptr) {
std::cout << what << std::endl;
}
template <typename T>
void print(T what, typename std::enable_if<my_is_char<T>::value>::type * = nullptr) {
std::cout << '\'' << what << '\'' << std::endl;
}
template <typename T>
void print(T what, typename std::enable_if<my_is_string<T>::value>::type * = nullptr) {
std::cout << '\"' << what << '\"' << std::endl;
}
int main() {
print(5.1);
print('a');
print(std::string("hello"));
}
advance()
Az std::advance
függvény n
lépéssel léptet előre egy iterátort:
auto it = valami_tarolo.begin();
std::advance(it, 5);
Ez annyiban okosabb egy n-szer futó, ++it
törzsű ciklusnál, hogy véletlen
elérésű iterátorok esetén automatikusan, fordítási időben it+=n
-né változik.
Írd meg a saját advance()
függvényedet dispatcher függvény segítségével!
Megoldás
#include <iterator>
#include <vector>
#include <list>
#include <iostream>
template <typename ITER>
void my_advance(ITER & it, int n, std::random_access_iterator_tag) {
std::cout << "O(1) :)\n";
it += n;
}
template <typename ITER>
void my_advance(ITER & it, int n, std::forward_iterator_tag) {
std::cout << "O(n) :(\n";
for (int i = 0; i < n; ++i)
++it;
}
template <typename ITER>
void my_advance(ITER & it, int n) {
using tag = typename std::iterator_traits<ITER>::iterator_category;
my_advance(it, n, tag());
}
int main() {
std::vector<int> v = { 1, 2, 3, 4, 5, 6, 7 };
auto iv = v.begin();
my_advance(iv, 5);
std::list<int> l = { 1, 2, 3, 4, 5, 6, 7 };
auto il = l.begin();
my_advance(il, 5);
}
C++17: írd meg a saját advance()
függvényedet if constexpr
segítségével!
Vajon jobb vagy rosszabb ez a megoldás, mint a fenti?
Megoldás
#include <iterator>
#include <vector>
#include <list>
#include <iostream>
template <typename ITER>
void my_advance(ITER & it, int n) {
using tag = typename std::iterator_traits<ITER>::iterator_category;
if constexpr (std::is_base_of<std::random_access_iterator_tag, tag>::value) {
std::cout << "O(1) :)\n";
it += n;
} else {
std::cout << "O(n) :(\n";
for (int i = 0; i < n; ++i)
++it;
}
}
int main() {
std::vector<int> v = { 1, 2, 3, 4, 5, 6, 7 };
auto iv = v.begin();
my_advance(iv, 5);
std::list<int> l = { 1, 2, 3, 4, 5, 6, 7 };
auto il = l.begin();
my_advance(il, 5);
}
A feladattal kapcsolatban lásd még a Concepts előadás ide vágó részét! (Pár héttel később jelenik meg, mint ez a feladat először.)
Mit csinál a függvény?
Adott az alábbi kódrészlet. Ebben a feladatban ezt a függvénypárost felhasználva kell dolgozni.
template <typename T, decltype(std::begin(*static_cast<T *>(nullptr))) * = nullptr>
constexpr bool mystery_func(T const *) {
return true;
}
constexpr bool mystery_func(void const *) {
return false;
}
- Vajon mire jók a függvények? Hogyan kell őket használni, és mit mutat meg a visszatérési értékük?
- Tedd be a függvényeket egy parametrizálható segédosztályba, amelynek statikus
value
adattagja megmondja, hogy a példányosító típusa rendelkezik-e azzal a bizonyos tulajdonsággal vagy nem, amelyet ezek a függvények tesztelnek. - Írj egy
print()
függvénysablont, amely tetszőleges típusú paramétert átvehet (konstans referenciával), és annak kiíró operátorát<<
használva kiírja a tartalmát azstd::cout
-ra! - Az
std::enable_if
segédosztály használatával specializáld ezt aprint()
függvénysablont arra az esetre, amikor a példányosító típus rendelkezik amystery_func()
által tesztelt tulajdonsággal, és arra, amikor nem! - Működik tömbre is az így megírt függvényed? Miért?
- Mi a különbség az alábbi deklarációk között?
template <typename T, decltype(std::begin(*static_cast<T *>(nullptr))) * = nullptr> bool mystery_func(T const *); template <typename T, typename = decltype(std::begin(*static_cast<T *>(nullptr)))> bool mystery_func(T const *); template <typename T, size_t = sizeof(std::begin(*static_cast<T *>(nullptr)))> bool mystery_func(T const *);
Megoldás
A függvények azt tesztelik, hogy a nekik adott típusú objektumnak van-e iterátora, vagy nincs. Ezt a sablonfüggvény
úgy éri el, hogy az std::begin
függvénynek átad egy képzeletbeli objektumot. Ha az std::begin()
nem paraméterezhető azzal a típussal, a SFINAE szabály miatt a deklarációt a fordító eldobja. A null értékű pointer
dereferálásával nincs gond, mivel a decltype()
belseje kiértékeletlen környezet. Mindez tömbre is
működik, mert az std::begin
-nek létezik a tömbökre specializált változata.
A három deklaráció között semmilyen érdemi különbség nincs, mindegyik észrevétlen és névtelen sablonparamétert hoz létre, és mindegyik aktiválja a SFINAE szabályt.
#include <iostream>
#include <type_traits>
template <typename TYPE>
class HasIterator {
private:
template <typename T, decltype(std::begin(*static_cast<T *>(nullptr))) * = nullptr>
static constexpr bool has_iterator(T const *) {
return true;
}
static constexpr bool has_iterator(void const *) {
return false;
}
public:
static constexpr bool value = has_iterator(static_cast<TYPE *>(nullptr));
};
template <typename T, typename std::enable_if<!HasIterator<T>::value>::type * = nullptr>
void print(T const & what) {
std::cout << what;
}
template <typename T, typename std::enable_if<HasIterator<T>::value>::type * = nullptr>
void print(T const & what) {
std::cout << "{";
for (auto const & elem : what) {
print(elem);
std::cout << ", ";
}
std::cout << "}";
}
int main() {
int i = 2;
int a[] = { 1, 2, 3 };
print(i);
print(a);
}
Érték vagy referencia?
Azt szoktuk mondani, hogy az érték paraméterű függvényekkel szemben a konstans referencia paraméterű függvényeket preferáljuk: ne másoljuk le az objektumot, ha nem muszáj, mert a felesleges másolások lassítják a programot. No igen, de ez csak nagyobb objektumokra igaz; az alábbi függvény éppen a referenciák miatt lassú. Mert ahelyett, hogy átadnánk a számok értékét (valószínűleg a processzor egy regiszterében), címeket adunk át, és fölösleges memóriaolvasási műveletekre kényszerítjük a gépet:
int const& greater(int const& a, int const& b) {
return a > b ? a : b;
}
Beépített típusok esetén az érték szerinti paraméterátadás a gyorsabb:
int greater(int a, int b) {
return a > b ? a : b;
}
A feladat: írj két sablonfüggvényt, amelyek tetszőleges, T
típusú objektumok közül a nagyobbikkal
térnek vissza! Az egyik dolgozzon referenciákkal, a másik értékekkel! A type_traits
fejlécfájl segédosztályaival
oldd meg, hogy beépített típusokkal való példányosítás esetén az utóbbi, osztályokkal való példányosítás esetén
az előbbi hívódjon!
Vissza kellett olvasnod a szövegben, hogy melyik az „előbbi” és melyik az „utóbbi”? Ha az lenne odaírva, hogy „beépített típusoknál érték szerint, osztályoknál cím szerinti paraméterátadás legyen”, akkor nem kellett volna. Ugyanígy időbe telik a gépnek is egy referencia dereferálása, ha nem lehetett kioptimalizálni: extra memóriaművelet.
Megoldás
#include <type_traits>
#include <iostream>
#include <string>
template <typename T,
typename = typename std::enable_if<!std::is_class<T>::value>::type>
T greater(T a, T b) {
std::cout << "value\n";
return a > b ? a : b;
}
template <typename T,
typename = typename std::enable_if<std::is_class<T>::value>::type>
T const& greater(T const& a, T const& b) {
std::cout << "const ref\n";
return a > b ? a : b;
}
int main() {
std::cout << greater(3, 4) << std::endl;
std::cout << greater(std::string{"alma"}, std::string{"korte"}) << std::endl;
}
Megoldás után lásd még a következő feladatot.
Fordítási hiba
Alább egy max()
függvény, amelyik két érték közül a nagyobbikat
választaná ki – beépített típusok esetén érték, objektumok esetén referencia szerint.
#include <type_traits>
#include <iostream>
#include <string>
template <typename T, typename = typename std::enable_if<std::is_class<T>::value>::type>
T max(T a, T b) {
std::cout << "val\n";
return a > b ? a : b;
}
template <typename T, typename = typename std::enable_if<!std::is_class<T>::value>::type>
T const& max(T const& a, T const& b) {
std::cout << "cref\n";
return a > b ? a : b;
}
int main() {
int i = 3, j = 4;
std::cout << max(i, j) << std::endl;
std::string a = "alma", b = "korte";
std::cout << max(a, b) << std::endl;
}
- Jó ötlet ez? Miért nem? :)
- Miért nem fordul le a kód?
Lásd még az előző feladatot.
Megoldás
Nem biztos, hogy jó ötlet. Hogy referenciaként vagy másolatként látjuk a nagyobbik értéket,
az nem attól kellene függjön, hogy milyen típusú. Lehet, a függvény használója kíváncsi lenne
egy tömbelem identitására is, de ha a tömb int
-eket tartalmaz, így nem tud
választani közülük.
A fordítási hibát az std::max
függvény létezése okozza. A max(a, b)
hívásnál az a
és b
változó típusa std
névtérbeli std::string
,
ezért a fordító automatikusan az std
névtérbeli függvények között is keres,
a Koenig-féle szabálynak megfelelően.