Share via


Převody typů definovaných uživatelem (C++)

Převod vytvoří novou hodnotu určitého typu z hodnoty jiného typu. Standardní převody jsou integrovány do jazyka C++ a podporují jeho předdefinované typy a můžete vytvářet uživatelsky definované převody pro převody do, z nebo mezi uživatelem definovanými typy.

Standardní převody provádějí převody mezi integrovanými typy, mezi ukazateli nebo odkazy na typy související s dědičností, z a z ukazatelů void a na ukazatel null. Další informace naleznete v tématu Standardní převody. Převody definované uživatelem provádějí převody mezi typy definovanými uživatelem nebo mezi uživatelem definovanými typy a integrovanými typy. Můžete je implementovat jako konstruktory převodu nebo jako převodní funkce.

Převody můžou být explicitní – když programátor volá jeden typ, který má být převeden na jiný, jako při přetypování nebo přímé inicializaci – nebo implicitně – když jazyk nebo program volá jiný typ než ten, který program dává programátor.

Při pokusu o implicitní převody:

  • Argument zadaný funkci nemá stejný typ jako odpovídající parametr.

  • Hodnota vrácená funkcí nemá stejný typ jako návratový typ funkce.

  • Výraz inicializátoru nemá stejný typ jako objekt, který inicializuje.

  • Výraz, který řídí podmíněný příkaz, konstruktor smyčky nebo přepínač, nemá typ výsledku, který je nutný k jeho řízení.

  • Operand zadaný operátorem nemá stejný typ jako odpovídající operand-parametr. U předdefinovaných operátorů musí mít oba operandy stejný typ a jsou převedeny na společný typ, který může představovat obojí. Další informace naleznete v tématu Standardní převody. U uživatelem definovaných operátorů musí mít každý operand stejný typ jako odpovídající operand-parametr.

Pokud jeden standardní převod nemůže dokončit implicitní převod, kompilátor může k jeho dokončení použít uživatelsky definovaný převod následovaný dodatečným standardním převodem.

Pokud jsou v lokalitě převodu k dispozici dva nebo více uživatelsky definovaných převodů, které provádějí stejný převod, je převod nejednoznačný. Takové nejednoznačnosti jsou chybou, protože kompilátor nemůže určit, který z dostupných převodů by měl zvolit. Nejedná se ale o chybu, která by pouze definovala několik způsobů provedení stejného převodu, protože sada dostupných převodů se může lišit v různých umístěních ve zdrojovém kódu – například v závislosti na tom, které soubory hlaviček jsou součástí zdrojového souboru. Pokud je v lokalitě převodu k dispozici pouze jeden převod, neexistuje nejednoznačnost. Existuje několik způsobů, jak může dojít k nejednoznačným převodům, ale nejběžnější jsou:

  • Více dědičnosti. Převod je definován ve více než jedné základní třídě.

  • Nejednoznačné volání funkce Převod je definován jako konstruktor převodu cílového typu a jako funkce převodu zdrojového typu. Další informace naleznete v tématu Převodní funkce.

Nejednoznačnost můžete obvykle vyřešit tím, že opravíte název příslušného typu úplněji nebo provedete explicitní přetypování, abyste svůj záměr objasnili.

Konstruktory převodu i převodní funkce dodržují pravidla řízení přístupu členů, ale přístupnost převodů se považuje pouze v případě, že lze určit jednoznačný převod. To znamená, že převod může být nejednoznačný, i když by úroveň přístupu konkurenčního převodu zabránila jeho použití. Další informace o přístupnosti členů naleznete v tématu Řízení přístupu členů.

Explicitní klíčové slovo a problémy s implicitním převodem

Ve výchozím nastavení při vytváření uživatelem definovaného převodu ho kompilátor může použít k provádění implicitních převodů. Někdy je to to, co chcete, ale jindy jsou to jednoduchá pravidla, která kompilátor při provádění implicitních převodů vedou k přijetí kódu, na který nechcete.

Jedním dobře známým příkladem implicitního převodu, který může způsobit problémy, je převod na bool. Existuje mnoho důvodů, proč byste mohli chtít vytvořit typ třídy, který se dá použít v logickém kontextu – například tak, aby ho bylo možné použít k řízení if příkazu nebo smyčky – ale když kompilátor provede převod definovaný uživatelem na předdefinovaný typ, může kompilátor později použít další standardní převod. Záměrem tohoto dalšího standardního převodu je umožnit akce, jako je povýšení z short do int, ale otevře také dveře pro méně běžné převody – například z bool do int, což umožňuje použití typu třídy v celočíselných kontextech, které jste nikdy nezamýšleli. Tento konkrétní problém se označuje jako Sejf Bool Problem. Tento druh problému spočívá v tom, že explicit klíčové slovo může pomoct.

Klíčové explicit slovo říká kompilátoru, že zadaný převod nelze použít k provádění implicitních převodů. Pokud jste chtěli syntaktické pohodlí implicitních převodů před explicit zavedením klíčového slova, museli jste buď přijmout nezamýšlené důsledky, které implicitní převod někdy vytvořil, nebo použít méně pohodlné pojmenované funkce převodu jako alternativní řešení. Teď pomocí klíčového explicit slova můžete vytvořit pohodlné převody, které lze použít pouze k provádění explicitních přetypování nebo přímé inicializace, a které nebudou vést k druhu problémů exemplifikovaných Sejf Bool Problem.

Klíčové explicit slovo lze použít u konstruktorů převodu od jazyka C++98 a pro převodní funkce od jazyka C++11. Následující části obsahují další informace o tom, jak klíčové slovo používat explicit .

Konverzní konstruktory

Konstruktory převodu definují převody z uživatelem definovaných nebo předdefinovaných typů na uživatelem definovaný typ. Následující příklad ukazuje konstruktor převodu, který převádí z integrovaného typu double na uživatelem definovaný typ Money.

#include <iostream>

class Money
{
public:
    Money() : amount{ 0.0 } {};
    Money(double _amount) : amount{ _amount } {};

    double amount;
};

void display_balance(const Money balance)
{
    std::cout << "The balance is: " << balance.amount << std::endl;
}

int main(int argc, char* argv[])
{
    Money payable{ 79.99 };

    display_balance(payable);
    display_balance(49.95);
    display_balance(9.99f);

    return 0;
}

Všimněte si, že první volání funkce display_balance, která přebírá argument typu Money, nevyžaduje převod, protože jeho argument je správný typ. Nicméně při druhém volání display_balance, převod je potřeba, protože typ argumentu, s double hodnotou 49.95, není to, co funkce očekává. Funkce nemůže tuto hodnotu použít přímo, ale protože existuje převod z typu argumentu –double na typ odpovídajícího parametru –Money je z argumentu vytvořena dočasná hodnota typu Money a slouží k dokončení volání funkce. Ve třetím volání display_balancesi všimněte, že argument není , doubleale je to float hodnota 9.99– a přesto je volání funkce stále možné dokončit, protože kompilátor může provést standardní převod – v tomto případě od – do doublefloat – a potom provést převod definovaný double uživatelem až Money po dokončení potřebného převodu.

Deklarace konstruktorů převodu

Pro deklarování konstruktoru převodu platí následující pravidla:

  • Cílový typ převodu je uživatelem definovaný typ, který se konstruuje.

  • Konstruktory převodu obvykle přebírají přesně jeden argument, což je typ zdroje. Konstruktor převodu však může zadat další parametry, pokud má každý další parametr výchozí hodnotu. Typ zdroje zůstává typem prvního parametru.

  • Konstruktory převodu, jako jsou všechny konstruktory, nezadávají návratový typ. Určení návratového typu v deklaraci je chyba.

  • Konstruktory převodu mohou být explicitní.

Explicitní konverzní konstruktory

Deklarováním konstruktoru explicitpřevodu lze jej použít pouze k provedení přímé inicializace objektu nebo k provedení explicitního přetypování. To zabraňuje funkcím, které přijímají argument typu třídy, aby implicitně přijímaly i argumenty zdrojového typu konstruktoru převodu a brání tomu, aby byl typ třídy inicializován z hodnoty zdrojového typu. Následující příklad ukazuje, jak definovat explicitní konverzní konstruktor a účinek, který má na to, jaký kód je dobře vytvořen.

#include <iostream>

class Money
{
public:
    Money() : amount{ 0.0 } {};
    explicit Money(double _amount) : amount{ _amount } {};

    double amount;
};

void display_balance(const Money balance)
{
    std::cout << "The balance is: " << balance.amount << std::endl;
}

int main(int argc, char* argv[])
{
    Money payable{ 79.99 };        // Legal: direct initialization is explicit.

    display_balance(payable);      // Legal: no conversion required
    display_balance(49.95);        // Error: no suitable conversion exists to convert from double to Money.
    display_balance((Money)9.99f); // Legal: explicit cast to Money

    return 0;
}

V tomto příkladu si všimněte, že stále můžete použít explicitní konverzní konstruktor k provedení přímé inicializace payable. Pokud byste místo toho chtěli zkopírovat a inicializovat Money payable = 79.99;, jednalo by se o chybu. První volání display_balance není ovlivněno, protože argument je správný typ. Druhé volání display_balance je chyba, protože konstruktor převodu nelze použít k provádění implicitních převodů. Třetí volání display_balance je legální z důvodu explicitního přetypování Money, ale všimněte si, že kompilátor stále pomohl dokončit přetypování vložením implicitního přetypování z float do double.

I když usnadnění povolení implicitních převodů může být lákavé, může to představovat těžko najít chyby. Pravidlem palce je, aby všechny konstruktory převodu explicitně s výjimkou případů, kdy jste si jisti, že chcete provést konkrétní převod implicitně.

Převodní funkce

Převodní funkce definují převody z uživatelem definovaného typu na jiné typy. Tyto funkce se někdy označují jako "operátory přetypování", protože spolu s konstruktory převodu se volají, když je hodnota přetypována na jiný typ. Následující příklad ukazuje funkci převodu, která převádí z uživatelem definovaného typu , Moneyna předdefinovaný typ, double:

#include <iostream>

class Money
{
public:
    Money() : amount{ 0.0 } {};
    Money(double _amount) : amount{ _amount } {};

    operator double() const { return amount; }
private:
    double amount;
};

void display_balance(const Money balance)
{
    std::cout << "The balance is: " << balance << std::endl;
}

Všimněte si, že členová proměnná amount je soukromá a že funkce veřejného převodu na typ double je zavedena pouze k vrácení hodnoty amount. Ve funkci display_balancedojde k implicitnímu převodu, pokud je hodnota balance streamována do standardního výstupu pomocí operátoru <<vložení datového proudu . Vzhledem k tomu, že pro uživatelem definovaný typ Moneynení definován žádný operátor vložení datového proudu , ale existuje jeden pro předdefinovaný typ double, kompilátor může funkci převodu Money použít k double uspokojení operátoru vložení datového proudu.

Převodní funkce jsou zděděné odvozenými třídami. Převodní funkce v odvozené třídě přepíší pouze zděděnou převodní funkci při převodu na přesně stejný typ. Například uživatelem definovaná převodní funkce operátoru odvozené třídy int nepřepíše (nebo dokonce ovlivňuje) uživatelsky definovanou převodní funkci operátoru základní třídy short, i když standardní převody definují vztah převodu mezi int a short.

Deklarace funkcí převodu

Pro deklarování funkce převodu platí následující pravidla:

  • Cílový typ převodu musí být deklarován před deklarací funkce převodu. Třídy, struktury, výčty a typedefs nelze deklarovat v rámci deklarace funkce převodu.

    operator struct String { char string_storage; }() // illegal
    
  • Funkce převodu nepřijímají argumenty. Zadání libovolných parametrů v deklaraci je chyba.

  • Funkce převodu mají návratový typ určený názvem funkce převodu, což je také název cílového typu převodu. Určení návratového typu v deklaraci je chyba.

  • Převodní funkce můžou být virtuální.

  • Převodní funkce můžou být explicitní.

Explicitní převodní funkce

Pokud je funkce převodu deklarována jako explicitní, lze ji použít pouze k provedení explicitního přetypování. To brání funkcím, které přijímají argument cílového typu funkce převodu, aby implicitně přijímaly i argumenty typu třídy a brání tomu, aby instance cílového typu byly inicializovány z hodnoty typu třídy. Následující příklad ukazuje, jak definovat explicitní převodní funkci a efekt, který má na to, jaký kód je dobře vytvořený.

#include <iostream>

class Money
{
public:
    Money() : amount{ 0.0 } {};
    Money(double _amount) : amount{ _amount } {};

    explicit operator double() const { return amount; }
private:
    double amount;
};

void display_balance(const Money balance)
{
    std::cout << "The balance is: " << (double)balance << std::endl;
}

Zde byl operátor převodní funkce dvojitý a explicitní přetypování na typ double byl zaveden ve funkci display_balance k provedení převodu. Pokud bylo toto přetypování vynecháno, kompilátor nemůže najít vhodný operátor << vložení datového proudu pro typ Money a dojde k chybě.