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.
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?
> Krok #2
Co możemy zamiast tego zrobić?
> 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!.

