приколы с операторами

Dizer

✩✩✩✩✩✩✩
3 Янв 2022
7
0
нужно чтобы к классу Test можно было обращаться как к массиву.
C++:
class Test {
public:
    int* array;
    ~Test() { delete[]array; }

    Test(int x) {
        array = new int[x] {0};
    }
private:
};

int main() {
    Test a(10);    //класс в котором массив на 10 ячеек

    //вот так должно выглядеть
    a[1] = -1;
    cout << a[1];

    return 0;
}
 

bort707

★★★★★★✩
21 Сен 2020
2,898
862
И где тут операторы?
Хотите обращаться к классу как массиву - начните с переопределения оператора []
 

Dizer

✩✩✩✩✩✩✩
3 Янв 2022
7
0
@bort707, Да, я хочу обращаться к классу как массиву. Я могу написать оператор чтобы получить из массива значение
C++:
int operator[](int x) {
        return array[x];
}
но не знаю как присвоить этому массиву значение
C++:
a[1] = -1;  //ошибка
 

bort707

★★★★★★✩
21 Сен 2020
2,898
862
Так вы создали оператор для извлечения значения, а для присваивания где?
 

Dizer

✩✩✩✩✩✩✩
3 Янв 2022
7
0
@bort707,в этом и прикол. я не знаю как его описать. Если вас не затруднит напишите его.
 

Kir

★✩✩✩✩✩✩
28 Мар 2020
69
16
@Dizer,
Нужно возвращать не значение, а ссылку: int& operator[](int x) { return array[x]; }
Однако принимать int в качестве аргумента не самая лучшая идея, так как могут передать отрицательное значение, поэтому лучше сделать так:
int& operator[](size_t x) { return array[x]; }, кроме того, было бы не плохо проверить выход за границу массива.

PS: Если у вас нет задачи написать свой std::vector, то лучше использовать std::vector, который все это реализует, и даже больше, и надежнее.
 
  • Лойс +1
Реакции: Dizer

Dizer

✩✩✩✩✩✩✩
3 Янв 2022
7
0
@Kir,
Спасибо.
По сути да, я хотел переписать std::vector, но только в ознакомительных целях.
 

Kir

★✩✩✩✩✩✩
28 Мар 2020
69
16
@bort707, size_t - это переносимый тип, которого гарантированно хватит для хранения размера любого непрерывного участка памяти для конкретной машины, а значит гарантировано подойдет для индексирования массива любого размера, на конкретной машине. Кроме того оператор sizeof возвращает значение типа std::size_t, можно об этом почитать в стандарте на язык с++.
 

SergejEU

★★✩✩✩✩✩
16 Сен 2020
120
70
size_t - это переносимый тип, которого гарантированно хватит для хранения размера любого непрерывного участка памяти для конкретной машины, а значит гарантировано подойдет для индексирования массива любого размера
если вы печётесь о переносимости типов, то почему сразу не использовать базовый тип ptrdiff_t, который для этой цели прямо и создан? Зачем останавливаться на пол-пути?
 

Kir

★✩✩✩✩✩✩
28 Мар 2020
69
16
@SergejEU, ptrdiff_t - это знаковый тип, а в данном случае нужен беззнаковый.
 

SergejEU

★★✩✩✩✩✩
16 Сен 2020
120
70
@Kir,
ваша отмазка не катит. В посте #6 вы вещаете о типе int, а вам писал о другом - ptrdiff_t. Можно ведь было сделать домашнее задание, например почитать стандарты о Переносимости типов size_t и ptrdiff_t или Безопасность типов ptrdiff_t и size_t в адресной арифметике? Зачем вдруг стал нужен знаковый тип?

Если вы беретесь учить других, то будьте добры учить их корректно, а не заводить людей на минное поле с граблями.
 
Изменено:

Kir

★✩✩✩✩✩✩
28 Мар 2020
69
16
@SergejEU, хорошо, давайте обсудим:
ptrdiff_t -выдержка из стандарта "The type ptrdiff_t is an implementation-defined signed integer type that can hold the difference of two subscripts in an array object, as described in 7.6.6."
size_t - выдержка из стандарта "The type size_t is an implementation-defined unsigned integer type that is large enough to contain the size in bytes of any object (7.6.2.4)"

задача, переопределить operator[], для класса, внутри которого инкапсулирован массив, в качестве аргумента принимаем индекс, по которому нужно получить элемент массива.

Если в качестве аргумента использовать переменную ptrdiff_t, то может прилететь отрицательное значение, а взятие отрицательного индекса это UB, пояснять почему надеюсь не стоит. Чтобы защититься нужно сделать проверку на отрицательный индекс, и проверку на выход за границу максимально доступного сейчас индекса, так как выход за верхнюю границу - это тоже UB.

Если в качестве аргумента использовать переменную size_t, если кто-то попытается отправить отрицательное значение, оно будет неявно приведено к беззнаковому типу, таким образом нужно сделать только одну проверку на выход за верхнюю границу.

Сравним, что выгоднее сделать 2 проверки, или 1, мне лично кажется что 1 делать дешевле.
Предлагаю заглянуть в реализацию std::vector, ведь именно его пытались повторить, что там видим:
C++:
      _GLIBCXX_NODISCARD _GLIBCXX20_CONSTEXPR
      reference
      operator[](size_type __n) _GLIBCXX_NOEXCEPT
      {
    __glibcxx_requires_subscript(__n);
    return *(this->_M_impl._M_start + __n);
      }
а что значит size_type
C++:
 typedef size_t size_type;
Авторы стандартной библиотеки почему-то тоже считают, что лучше использовать size_t
При этом, что интересно, в реализации operator[] нет проверки выхода за границы массива, однако, в комментарии который я не стал вставлять, но который можно найти в исходнике, говорят, что в методе at() есть проверка, предлагаю заглянуть туда:

C++:
    protected:
      /// Safety check used only from at().
      _GLIBCXX20_CONSTEXPR
      void
      _M_range_check(size_type __n) const
      {
    if (__n >= this->size())
      __throw_out_of_range_fmt(__N("vector::_M_range_check: __n "
                       "(which is %zu) >= this->size() "
                       "(which is %zu)"),
                   __n, this->size());
      }

    public:
      /**
       *  @brief  Provides access to the data contained in the %vector.
       *  @param __n The index of the element for which data should be
       *  accessed.
       *  @return  Read/write reference to data.
       *  @throw  std::out_of_range  If @a __n is an invalid index.
       *
       *  This function provides for safer data access.  The parameter
       *  is first checked that it is in the range of the vector.  The
       *  function throws out_of_range if the check fails.
       */
      _GLIBCXX20_CONSTEXPR
      reference
      at(size_type __n)
      {
    _M_range_check(__n);
    return (*this)[__n];
      }
Действительно есть проверка.

PS: В следующий раз, если знаете как сделать лучше, просто предложите, вместо того, чтобы публично оценивать чью-то компетенцию, указывая на не сделанное домашнее задание, или ещё что-то в этом духе, хотя бы потому, что может случайно выясниться, что кто-то, тоже иногда пренебрегает выполнением домашних работ.
 

SergejEU

★★✩✩✩✩✩
16 Сен 2020
120
70
@Kir, давайте обсудим.. Я бы промолчал и не стал влезать в обсуждение, но вы сами противоречите себе. Если бы вы написали, что как один из вариантов переопределения оператора я вижу так-то и так-то, то никаких проблем, это ваше право. Но вы идете дальше и предлагаете, цитирую #6: "кроме того, было бы не плохо проверить выход за границу массива". Что это значит, а то, что необходимо применить оператор сравнения с неявным или явным приведением типов; необходимо применить адресную арифметику - посчитать разность указателей, потом сравнить полученный результат. И тут возможны варианты и самые неожиданные сюпризы, способные возникнуть на некоторых платформах и для некоторых настройках компилятора. И как вы решили сравнивать безнаковое целое и отрицательный результат, да еще, если это происходит в неявном виде? Так где же ваша хваленная переносимость? Если вы собрались говорить А, то нужно говорить и Б.
 

poty

★★★★★★✩
19 Фев 2020
2,988
895
@SergejEU, не очень понимаю, причём тут разность указателей и адресная арифметика? Размер массива определяется на этапе его создания (и даже если потом изменяется - всё равно известен явно). Это значит, что нужно обеспечить (при unsigned) сравнение переданного в класс индекса только с максимальным значением, которое равно размеру массива минус единица. Мы же оперируем "элементами массива", а не байтами в памяти.
 
  • Лойс +1
Реакции: Kir

SergejEU

★★✩✩✩✩✩
16 Сен 2020
120
70
не очень понимаю, причём тут разность указателей и адресная арифметика?
Потому что индексация в массивах с использованием квадратных скобок – это всего лишь иная запись адресной арифметики. Всякий раз, обращаясь к элетантам массива, мы выполняем действия адресной арифметики. Это как в пьесе Мольера один персонаж - господин Журден не знал, что всю свою жизнь он говорил именно прозой.

Не важно был или не был заранее известен размер массива, все равно придется сравнивать указатели на.. Вот наглядный пример, где из-за неявного приведения типов, код работает на 32-битной платформе, но не работает на 64-битной. И это еще цветочки, есть другие более содержательные примеры с операторами сравнения.

C++:
int A = -2;
unsigned B = 1;
int array[5] = { 1, 2, 3, 4, 5 };
int *ptr = array + 3;
ptr = ptr + (A + B); //Invalid pointer value on 64-bit platform
printf("%i\n", *ptr); //Access violation on 64-bit platform
 
Изменено:

poty

★★★★★★✩
19 Фев 2020
2,988
895
@SergejEU, во-первых, если индекс определяется одним типом, то никаких неявных преобразований не делается, в этом и была мысль использования size_t.
Во-вторых, предлагалось как раз отказаться от знаковых типов, который использован Вами в примере.
В-третьих, с чего Вы взяли, что физическое воплощение массива в классе будет соответствовать олдскульным нормам непрерывности выделенной области? А ведь именно это приводит к понятию эквивалентности адресной арифметики и индексации массива.
 

Kir

★✩✩✩✩✩✩
28 Мар 2020
69
16
@SergejEU, Вы забываете, что мы говорим о взаимодействии с классом, который синтаксически выглядит как взаимодействие с массивом, но по факту будет вызываться метод, в котором можно обработать возможные ошибки, перед фактическим обращением к "сырому" массиву.
Вот иллюстрация того, что будет происходить
 

SergejEU

★★✩✩✩✩✩
16 Сен 2020
120
70
Если в качестве аргумента использовать переменную size_t, если кто-то попытается отправить отрицательное значение, оно будет неявно приведено к беззнаковому типу, таким образом нужно сделать только одну проверку на выход за верхнюю границу.

Сравним, что выгоднее сделать 2 проверки, или 1, мне лично кажется что 1 делать дешевле.
Вы заблуждаетесь. Откуда вы взяли, что всегда и в всех возможных случаях приведенное итрицательное число/индекс к безнаковому окажется за пределами массива? Это может зависеть от настроек, платформы и использованого типа - char short int ect. Даёте гарантию, что этого не произойдёт никогда? Ну вот, преимущество size_t перед ptrdiff_t эфемерно. Соглашайтесь наконец и на этом закроем тему.
 

Kir

★✩✩✩✩✩✩
28 Мар 2020
69
16
@SergejEU,
Откуда вы взяли, что всегда и в всех возможных случаях приведенное итрицательное число/индекс к безнаковому окажется за пределами массива?
Представление отрицательных целых чисел

Это может зависеть от настроек, платформы и использованого типа - char short int ect
Поэтому и нужно использовать переносимые типы данных.


Ну вот, преимущество size_t перед ptrdiff_t эфемерно
Каждый тип имеет место применения, в конкретном случае преимущество size_t очевидное.
Очередной эфемерный пример

Соглашайтесь наконец и на этом закроем тему
Соглашаться с чем? С использованием типа ptrdiff_t для реализации operator[] как в std::vector, вместо size_t - не согласен, причины несогласия изложены в предыдущих постах, кроме того в библиотечной реализации std::vector используется size_t, впрочем об этом я также упоминал.
 

SergejEU

★★✩✩✩✩✩
16 Сен 2020
120
70
Соглашаться с чем?
Сначала согласитесь с вашим заблуждением. Для этого перечитайте мой пост #21 и запустите на исполнение привеведенный мною ниже контрпример. Если правы вы, цитата
Если в качестве аргумента использовать переменную size_t, если кто-то попытается отправить отрицательное значение, оно будет неявно приведено к беззнаковому типу, таким образом нужно сделать только одну проверку на выход за верхнюю границу.
то он подтвердит ваше предсказание, в противном случае вам следует повторить азы науки. Ничего в том зазорного нет, все мы когда-то заблуждаемся, но упорствовать и не признавать своих ошибок это уже перебор
C++:
// requires 64-bit platforms, GNU GCC
#include <iostream>
#include <cstdlib>

int main( void ) {
  
    long int index = -4294967296;
    size_t number = 18446744069414584320UL;
  
    std::cout << "number = " << number << std::endl;
    std::cout << "index = " << index << std::endl;

    if ( number < index ) {
        std::cout << "Your prediction is ok"  << std::endl;
    }
    else {
        std::cout << "go learn your lessons!" << std::endl;
    } 
  
    return 0;
}
P.S. Как видите ваша экономия на проверках, что можно обойтись одной проверкой на выход индекса за пределы массива не сработала. По-хорошему, необходима еще одна проверка на отрицательные значения и в этом смысле преимущество эфемерно. А ссылки на чужие проекты не аргумент, для начала нужно ознакомиться со спецификациями этих проектов, сравнить реквайментс и прочее.
 
Изменено:

Kir

★✩✩✩✩✩✩
28 Мар 2020
69
16
@SergejEU, чтож, продолжим.
Во-первых, я должен отметить, что вы пытаетесь выйти из контекста изначальной задачи. Ещё раз уточню, необходимо было реализовать operrator[], для итерирования по массиву, который является инвариантом класса. Т.е. обращение к массиву будет не прямое, а через метод. По контексту точно известно (так как этот массив создается и уничтожается внутри класса), итерироваться нужно только относительно 0-го элемента и только вверх, значит отрицательные значения не валидны, нужны только положительные, поэтому логично сделать сигнатуру с приемом в качестве аргумента беззнакового целого. Таким образом внутри функции operrator[] всегда будет положительное целое, даже, если в программе будут передавать отрицательное число, сигнатура функции не может поменяться в процессе выполнения, если конечно не будет соответствующей перегрузки, но такую перегрузку нет смысла делать, и в примере её нет.

Во-вторых, ссылка на чужой проект - это ссылка на исходник реализации стандартной библиотеки компилятора GCC. ТС сам написал, что, в учебных целях, хотел сделать свою реализацию, чтобы лучше разобраться в нюансах, поэтому ссылка на референс, я считаю, абсолютно уместна.

Теперь давайте разберем ваш контрпример, к которому сразу несколько замечаний:
1) Почему для переменной index в качестве типа используется long int, вы же за ptrdiff_t рассуждали все это время. И это имеет значение при переносе кода на другую платформу, об этом будет ещё сказано ниже.

2) Значения для index и number выбраны не удачные, ниже увидите почему.

Прежде всего обратимся к стандарту 7.4 Usual arithmetic conversions, на который я буду ссылаться.
Также я предлагаю добавить в ваш пример ещё проверку на равенство, посмотреть как теперь работает код на 64-разрядной платформе можно тут
C++:
// requires 64-bit platforms, GNU GCC
#include <iostream>
#include <cstdlib>

int main( void ) {
 
    long int index = -4294967296;
    size_t number = 18446744069414584320UL;
 
    std::cout << "number = " << number << std::endl;
    std::cout << "index = " << index << std::endl;

    if ( number < index ) {
        std::cout << "Your prediction is ok"  << std::endl;
    } else if (number == index) {
        std::cout << "Yep, these numbers are equal;"  << std::endl;
    } else {
        std::cout << "go learn your lessons!" << std::endl;
    }
 
    return 0;
}
Попробуем запустить, и увидим, что выполнится условие else if (number == index) . Почему так?
Конкретно для той платформы, которая выполняет этот код (см ссылку), conversion rank для типов long int и size_t (size_t в данном случае alias для unsigned long) одинаковый, что на это говорит стандарт:
if the operand that has unsigned integer type has rank greater than or equal to the rank of the type of the other operand, the operand with signed integer type shall be converted to the type of the operand with unsigned integer type.
Не буду переводить стандарт, но попрошу обратить внимание, на то, что в конкретном случае (конкретная платформа на которой запускался пример) представление index и number в памяти идентичное. Собственно поэтому они и равны, и ровно по этому неважно к какому типу будет приведение long int или size_t, кроме того, на платформах с представлением данных типов меньше 64бит, все эти значения будут срезаться до 0. И также будет выполняться условие else if (number == index) . Поэтому в том виде котором пример существует сейчас - забавен, но бесполезен, так как ничего конкретного не доказывает.

Я предлагаю изменить эти значения, на более интересные, ссылка на исполняемую платформу
C++:
// requires 64-bit platforms, GNU GCC
#include <iostream>
#include <cstdlib>

int main( void ) {
 
    long int index = -1;     
    size_t number = 65520UL; /* 65520 = 0xFFF0 */
 
    std::cout << "number = " << number << std::endl;
    std::cout << "index = " << index << std::endl;

    if ( number < index ) {
        std::cout << "Your prediction is ok"  << std::endl;
    } else if (number == index) {
        std::cout << "Yep, these numbers are equal;"  << std::endl;
    } else {
        std::cout << "go learn your lessons!" << std::endl;
    }
 
    return 0;
}
Теперь мы видим, что выполняется условие if ( number < index ). Почему так - потому что бинарное представление -1 на данной платформе соответствует 0xFFFFFFFFFFFFFFFF, а 0xFFFFFFFFFFFFFFFF явно больше чем 0xFFF0.
Таким образом уже точно можно сказать, выполняется пункт стандарта, который процитирован выше.
А теперь запустим этот код на другой платформе, на AVR. На AVR size_t - это uint16_t, а long int - int32_t, результат уже будет иной.
В стандарте такое результат объясняется следующим образом:
if the type of the operand with signed integer type can represent all of the values of the type of the operand with unsigned integer type, the operand with unsigned integer type shall be converted to the type of the operand with signed integer type.
Т.е. size_t был приведен к long int, но при этом осталось положительным, а index - отрицательное число, положительное число больше отрицательного - ожидаемый результат.

Разные результаты на разных платформах - это плохо, это значит код имеет плохую переносимость, и ваш пример, слегка доработанный - это очень явно показывает. Т.е. переносимый код - код который будет работать одинаково на разных платформах, и если long int заменить на ptrdiff_t, то, внезапно, примеры заработают одинаково.

Ничего в том зазорного нет, все мы когда-то заблуждаемся, но упорствовать и не признавать своих ошибок это уже перебор
Вот тут я с вам полностью согласен.
 

SergejEU

★★✩✩✩✩✩
16 Сен 2020
120
70
@Kir, а если так:
C++:
// requires 64-bit platforms, GNU GCC

#include <iostream>
#include <cstdlib>


int main( void ) {
   
    long int a = -4294967296; 
    size_t index = a;
    size_t number = 18446744069414584321UL; 
   
    std::cout << "number = " << number << std::endl;
    std::cout << "a = " << a << std::endl;

    if ( number < index ) {
        std::cout << "Your prediction"  << std::endl;
    } else if (number == index) {
        std::cout << "Yep, these numbers are equal;"  << std::endl;
    }
    else {
        std::cout << "go learn your lessons!" << std::endl;
    }   
   
    return 0;
}
Во-первых, я должен отметить, что вы пытаетесь выйти из контекста изначальной задачи.
извините, но это вы пытаетесь выкручиваться. Вы утверждали, что для безнакового целого типа достаточно одного единственного сравнения if, чтобы предотвратить выход индекса за пределы массива. Я привел контрпример, может быть не столь элегантный, но по сути. Вы пытаетесь опять юлить. Одно меня утешает, что, несомненно, польза от чтения документации есть.
 
Изменено: