21.10 — Przeciążenie nawiasów operator

Wszystkie przeciążone operatory, które widziałeś do tej pory, pozwalają zdefiniować typ parametrów operatora, ale nie liczbę parametrów (która jest stała w zależności od typu operatora). Przykładowo operator== zawsze przyjmuje dwa parametry, natomiast operator! zawsze bierze jednego. Operator nawiasów (operator()) jest szczególnie interesującym operatorem, ponieważ pozwala zmieniać zarówno typ ORAZ liczbę parametrów, jakie przyjmuje.

Należy pamiętać o dwóch rzeczach: po pierwsze, operator nawiasów musi być zaimplementowany jako funkcja składowa. Po drugie, w nieobiektowym C++ operator () służy do wywoływania funkcji. W przypadku klas operator() jest po prostu zwykłym operatorem, który wywołuje funkcję (o nazwie operator()), jak każdy inny przeciążony operator.

Przykład

Przyjrzyjmy się przykładowi, który pozwala na przeciążenie tego operatora:

class Matrix
{
private:
    double data[4][4]{};
};

Macierze są kluczowym elementem algebry liniowej i często są używane do modelowania geometrycznego i grafiki komputerowej 3D. W tym przypadku wystarczy zauważyć, że klasa Matrix jest dwuwymiarową tablicą liczb podwójnych o wymiarach 4 na 4.

W lekcji na temat Przeciążając operator indeksu dolnego, nauczyłeś się, że możemy przeciążyć operator[], aby zapewnić bezpośredni dostęp do prywatnej jednowymiarowej tablicy. Jednak w tym przypadku chcemy dostępu do prywatnej tablicy dwuwymiarowej. Przed wersją C++23 operator[] był ograniczony do pojedynczego parametru i dlatego nie wystarczał, aby umożliwić nam bezpośrednie indeksowanie tablicy dwuwymiarowej.

Jednakże, ponieważ operator () może przyjmować dowolną liczbę parametrów, możemy zadeklarować wersję operatora(), która przyjmuje dwa parametry indeksu w postaci liczb całkowitych i używać jej do uzyskiwania dostępu do naszej dwuwymiarowej tablicy. Oto przykład:

#include <cassert> // for assert()

class Matrix
{
private:
    double m_data[4][4]{};

public:
    double& operator()(int row, int col);
    double operator()(int row, int col) const; // for const objects
};

double& Matrix::operator()(int row, int col)
{
    assert(row >= 0 && row < 4);
    assert(col >= 0 && col < 4);

    return m_data[row][col];
}

double Matrix::operator()(int row, int col) const
{
    assert(row >= 0 && row < 4);
    assert(col >= 0 && col < 4);

    return m_data[row][col];
}

Teraz możemy zadeklarować macierz i uzyskać dostęp do jej elementów w następujący sposób:

#include <iostream>

int main()
{
    Matrix matrix;
    matrix(1, 2) = 4.5;
    std::cout << matrix(1, 2) << '\n';

    return 0;
}

który generuje wynik:

4.5

Teraz ponownie przeciążmy operator (), tym razem w sposób nieprzyjmujący żadnych parametrów:

#include <cassert> // for assert()
class Matrix
{
private:
    double m_data[4][4]{};

public:
    double& operator()(int row, int col);
    double operator()(int row, int col) const;
    void operator()();
};

double& Matrix::operator()(int row, int col)
{
    assert(row >= 0 && row < 4);
    assert(col >= 0 && col < 4);

    return m_data[row][col];
}

double Matrix::operator()(int row, int col) const
{
    assert(row >= 0 && row < 4);
    assert(col >= 0 && col < 4);

    return m_data[row][col];
}

void Matrix::operator()()
{
    // reset all elements of the matrix to 0.0
    for (int row{ 0 }; row < 4; ++row)
    {
        for (int col{ 0 }; col < 4; ++col)
        {
            m_data[row][col] = 0.0;
        }
    }
}

A oto nasz nowy przykład:

#include <iostream>

int main()
{
    Matrix matrix{};
    matrix(1, 2) = 4.5;
    matrix(); // erase matrix
    std::cout << matrix(1, 2) << '\n';

    return 0;
}

który generuje wynik:

0

Ponieważ operator () jest tak elastyczny, może kusić użycie go do wielu różnych celów. Jest to jednak zdecydowanie odradzane, ponieważ symbol () tak naprawdę nie wskazuje, co robi operator. W powyższym przykładzie lepiej byłoby zapisać funkcję kasowania jako funkcję o nazwie clear() lub Erase(), ponieważ matrix.erase() jest łatwiejsza do zrozumienia niż matrix() (która może zrobić wszystko!).

Uwaga: Począwszy od C++23, możesz używać operator[] z wieloma indeksami. Działa to tak samo jak operator() powyżej.

Zabawa z funktorami

Operator() jest również często przeciążana w celu implementacji funktorami (lub obiektu funkcyjnego), czyli klas działających podobnie do funkcji. Przewaga funktora nad normalną funkcją polega na tym, że funktory mogą przechowywać dane w zmiennych składowych (ponieważ są to klasy).

Oto prosty funktor:

#include <iostream>

class Accumulator
{
private:
    int m_counter{ 0 };

public:
    int operator() (int i) { return (m_counter += i); }

    void reset() { m_counter = 0; } // optional 
};

int main()
{
    Accumulator acc{};
    std::cout << acc(1) << '\n'; // prints 1
    std::cout << acc(3) << '\n'; // prints 4

    Accumulator acc2{};
    std::cout << acc2(10) << '\n'; // prints 10
    std::cout << acc2(20) << '\n'; // prints 30
    
    return 0;
}

Zauważ, że użycie naszego akumulatora wygląda tak samo jak normalne wywołanie funkcji, ale nasz obiekt akumulatora przechowuje skumulowaną wartość.

Fajną rzeczą w funktorach jest to, że możemy utworzyć wiele oddzielnych obiektów funktorów tyle, ile potrzebujemy i korzystamy z nich wszystkich jednocześnie. Funktory mogą mieć także inne funkcje składowe (np. reset()), które wykonują wygodne zadania.

Wnioski

Operator() jest czasami przeciążany dwoma parametrami w celu indeksowania tablic wielowymiarowych lub pobierania podzbioru tablicy jednowymiarowej (z dwoma parametrami definiującymi podzbiór do zwrócenia). Wszystko inne prawdopodobnie lepiej napisać jako funkcję składową o bardziej opisowej nazwie.

Operator() jest również często przeciążany w celu utworzenia funktorów. Chociaż proste funktory (takie jak powyższy przykład) są dość łatwe do zrozumienia, funktory są zwykle używane w bardziej zaawansowanych tematach programowania i zasługują na osobną lekcję.

Czas quizu

Pytanie nr 1

Napisz klasę o nazwie MyString, która przechowuje std::string. Przeciążenie operator<< , aby wyprowadzić ciąg. Przeciążenie operator() aby zwrócić podciąg rozpoczynający się od indeksu pierwszego parametru (jako MyString). Długość podciągu powinna być zdefiniowana przez drugi parametr.

Należy uruchomić następujący kod:

int main()
{
    MyString s { "Hello, world!" };
    std::cout << s(7, 5) << '\n'; // start at index 7 and return 5 characters

    return 0;
}

To powinno zostać wydrukowane

world

Wskazówka: Możesz użyć std::string::substr aby uzyskać podciąg std::string.

Pokaż rozwiązanie

Pytanie nr 2

To pytanie quizowe jest dodatkowym atutem.

> Krok #1

Dlaczego powyższe jest nieefektywne, jeśli nie musimy modyfikować zwrócony podciąg?

Pokaż rozwiązanie

> Krok #2

Co możemy zamiast tego zrobić?

Pokaż rozwiązanie

> Krok #3

Zaktualizuj operator() z poprzedniego rozwiązania quizu, aby zwrócić podciąg jako a std::string_view .

Wskazówka: std::string::substr() zwraca a std::string. std::string_view::substr() zwraca a std::string_view. Zachowaj szczególną ostrożność, aby nie zwrócić zwisających std::string_view!.

Pokaż wskazówkę

Pokaż wskazówkę

Pokaż rozwiązanie

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
259 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze