Именованные параметры C++. Не пригодились

    Время от времени вдруг начинает хотеться именованных параметров в C++. Не так давно была статья, да и сам какое-то время назад писал на эту тему. И вот что удивительно — со времен той своей статьи я участвую в новом проекте без необходимости тащить за собой старый код, и как-то удивительным образом всего этого описанного собой же не использую. Т.е. в вопросе разобрался, восхитился перспективами… и продолжил работать по-старинке! Как же так? Лень? Инерция? Ответ постараюсь дать под катом.

    Для начала рассмотрим пример — объявление функции, возвращающей объект даты для заданных дня, месяца и года.

    Date createDate(int day, int year, int month);
    

    Проблема очевидна — какой порядок параметров не выбери, через месяц, увидев подобное

    Date theDate = createDate(2, 3, 4);
    

    будешь гадать: «Что это? Второе марта 2004-го года, или четвертое 2002-го?». Если же особо повезло, и команда интернациональная, трактовка функции разработчиками может в корне отличаться. Одинаковые типы идут подряд в списке параметров… Вот в таких случаях обычно и хочется именованных параметров, которых в С++, увы, нет.

    Многим программистам приходится переключаться с одного языка программирования на другой. При этом что-то в новом языке нравится, что-то нет… Плохое со временем забывается, хорошее же хочется непременно перенести в ту среду, где сейчас работаешь. Вон, в том же Objective C именованные параметры имеются!

    + (UIColor *)colorWithRed:(CGFloat)red
                        green:(CGFloat)green
                         blue:(CGFloat)blue
                        alpha:(CGFloat)alpha
    

    Да, первый параметр идет без имени, но его название часто включают в имя метода. Решение, конечно, не идеальное (например, метода colorWithBlue не существует, такая вот цветовая несправедливость), зато именованные параметры поддерживаются на уровне языка. В VBA все еще лучше — имена можно дать всем параметрам, благодаря им можно не плодить множество похожих методов, а обойтись одним. Посмотрите, например, на Document.PrintOut

    А в C++ ничего такого нет! Сразу хочется исправлять ситуацию, искать библиотеки, придумывать костыли и велосипеды. И даже что-то найдется и получится. Но вместо этого можно задуматься, если все так прекрасно, почему именованные параметры не добавили. Столько парадигм поддерживается, а тут такое…

    Или, может, добавили? Просто назвали иначе. Например, пользовательскими типами. Самое время привести основную мысль статьи. Примитивным стандартным типам не место в интерфейсах реальной системы. Такие типы — просто строительные блоки, из которых нужно именно строить, а не пытаться жить внутри.

    Например, объект типа int — знаковое целое, лежащее в определенном диапазоне. Что описывает этот тип? Только свою реализацию. Это не может быть, например, количество яблок. Потому что яблок не может быть минус 10. Все еще хуже: unsigned int также непригоден для этой задачи. Потому что яблоки вообще не имеют никакого отношения к размерности типа данных на вашей платформе. Привязывая примитивные типы языка к параметрам открытых методов своих моделей, мы делаем ошибку, которую затем пытаемся «замять» с помощью различных костылей. Причем в своем стремлении скрыть оплошность мы часто игнорируем тот простой факт, что язык пытается нам помочь, говоря: «Я не поддерживаю этого напрямую, и неспроста...».

    Но главный недостаток примитивных типов — компилятор лишается шанса выявить логическую ошибку. Например, есть у нас метод, принимающий два параметра — имя и фамилию. Если свести их к стандартным строковым типам, то компилятор увидит только два куска текста, от перестановки которых смысл не поменяется. В результате один разработчик передаст первым параметром имя, а другой — фамилию. И оба технически будут правы. Ошибка уничтожается в зачатке, если для имени и фамилии существуют специальные типы. В реальной системе, где имя и фамилия являются до того значимыми сущностями, что входят в интерфейс по отдельности, сводить их просто к строкам — ошибка. Имя — это совсем не произвольная строка. Хотя бы потому, что выбирается из заранее известного множества. Еще оно, скажем, не содержит цифр (хотя тут я не уверен).

    Но вернемся к датам. День — это ни в коем случае не unsigned int, не unsigned char не даже std::string. День — это… день! Если мы строим модель, в которой присутствуют даты, то для представления дней имеет смысл создать специальный тип. Пользовательские типы данных — это как раз то, что придает C++ всю его мощь и выразительность. Тогда и костыли становятся не нужны. Вводим класс для представления дней

    class Day
    {
    	explicit Day (unsigned char day);
    	//...
    private:
    	unsigned char mValue;
    };
    

    Как-то вот так. Естественно для физического представления значения в памяти мы все равно используем примитивный тип. Но больше это не является частью интерфейса. Сразу же мы получаем полный контроль над содержимым этого поля, исключая ряд возможных ошибок. Да, не зная полной даты, точные ограничения установить не получится, но по крайней мере проверку на попадание в интервал 1..31 уже можно организовать. Самое же главное: реализовав специальные типы данных для месяца и года с явными (explicit) конструкторами для инициализации примитивными типами, мы получим именованные параметры, поддерживаемые непосредственно языком. Функцию теперь можно вызывать следующим образом

    Date theDate = createDate(Day(2), Month(3), Year(4));
    

    Никаких окольных путей, никаких дополнительных библиотек. Да, менять местами параметры такой подход не позволит, но это не так и важно. Главная миссия именованных параметров — исключить ошибки при вызове функций и повысить читабельность кода — осуществляется.

    В качестве бонуса получаем повышенную гибкость, если, скажем, захотелось задавать месяц еще и в строковом виде. Теперь можно не делать перегружаемый вариант createDate (это функция создания объекта даты, какое ей вообще дело до формата месяца). Вместо этого просто добавляется еще один явный конструктор для типа «месяц»

    class Month
    {
    	explicit Month(unsigned char month);
    	explicit Month(std::string month);
    	//...
    private:
    	unsigned char mValue;
    };
    

    Каждый теперь занимается своим делом — createDate создает дату, а класс Month интерпретирует и контролирует корректность значения месяца.

    Date theDate = createDate(Day(2), Month("Jan"), Year(4));
    

    Сразу хочется возразить — а не многовато ли лишних типов будет, если для каждого примитивного типа делать свой тип-обертку? Тут как посмотреть. Если вы студент, которому нужно побыстрее написать лабу, сдать ее и забыть, то да — много лишнего кода и потерянное время. Но если речь идет о реальной системе, в долгой и счастливой жизни которой вы заинтересованы, то называть пользовательские типы для сущностей, используемых в интерфейсе, лишними я бы не стал.

    Но как быть с пользовательскими типами? Что, например, делать, если какой-то метод принимает несколько объектов одинакового типа

    User user1, user2;
    //...
    someMethod(user1, user2);
    

    Здесь все зависит от контекста. Если все объекты равнозначны, то и проблемы нет — от порядка их передачи ничего не меняется. Чтобы подчеркнуть это, можно разве что передавать объекты, упакованными в массив или другой контейнер. Если же объекты неравнозначны, например, метод ставит в подчинение user2 объекту user1, то совсем нелишними будут специальные типы, отражающие роли объектов. Должны это быть обертки вокруг объектов пользователей (как в случае с примитивными типами) или проще создать специальные классы-наследники User, зависит от реализуемой системы. Важно каким-то образом выразить средствами языка различные роли user1 и user2, позволяя компилятору отлавливать ошибки, связанные с их возможной путаницей.

    Какой можно сделать вывод. Не нужно стремиться выхватить все самое лучшее из всех языков и засунуть это в один, и без того многострадальный C++. Важно уметь побороть инерцию при смене языка программирования. Скажем, да, в Луа можно присваивать значения сразу нескольким переменным

    x, y = getPosition()

    Сама идея прекрасна, но нужна ли она в C++. Вообще не нужна. Тут проще создать тип Position и присваивать значение его объекту. Язык — это инструмент, не более того. И из того, что инструменты иногда похожи, совсем не следует, что пользоваться ими нужно одинаково вплоть до мелочей.
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 86

      +11
      Отличное заключение!
        +8
        Строгая типизация и ограничение работы с памятью/указателями — где-то я такое уже слышал )
          +10
          Главная миссия именованных параметров — исключить ошибки при вызове функций и повысить читабельность кода — осуществляется.
          Если сужать миссию именованных параметров до такой — то выполняется. Но мне кажется что опциональность параметров — очень важная возможность. И её пользовательскими типами не решить. Иначе придётся либо писать функции со множеством параметров, либо всё равно городить костыли описанные по ссылкам в начале вашей статьи.
            0
            Тоже можно решить, по-моему. Трудно, конечно, говорить в общем случае, но параметры, передаваемые в функцию обычно как-то логически связаны. А значит их можно объединить в один тип (или несколько). После этого можно инициализировать только нужные поля объекта этого типа и передавать в подпрограмму. Да, вызов в одну строчку может потеряться, но это не самая страшная потеря, особенно если ее ценой получится увернуться от тяжелых костылей.
              +1
              В плюсах же есть возможность указывать аргументу значение по умолчанию или речь о чем-то другом идет?
                +1
                Да, но нельзя сделать так
                CreateOptions(2, 3, Default, 8);
                
                  +2
                  Заметив больше чем 2-3 параметра функции, я обычно начинаю задумываться " А все ли я делаю правильно ?". Потом либо как-то разбиваю функцию, либо создаю структурку типа params для функции. Да и перемешивание и пропуски параметров вносят какую-то бардачность в код. Это лично мое ИМХО не претендующее на истинность.
                    +1
                    Не совсем функция, но для примера сойдет. Скажем, когда мы пишем
                    std::unordered_set<std::string>
                    
                    после подстановки параметров по умолчанию получается:
                    std::unordered_set<std::string, std::hash<std::string>, std::equal_to<std::string>,
                        std::allocator<std::string>>
                    

                    Если мы хотим создать эту же структуру только с нашим аллокатором (поменять последний параметр), то нам прийдется явно выписывать все эти параметры:
                    std::unordered_set<std::string, std::hash<std::string>, std::equal_to<std::string>,
                        my_allocator<std::string>>
                    
                    вместо какой-нибудь такой записи:
                    std::unordered_set<std::string, Allocator : my_allocator<std::string>>
                    
                  +2
                  да, но так как все параметры позиционные, а не именные нельзя пропустить несколько, а после них несколько задать.
                  Т.е.
                  void foo(int *ptr = NULL, int int_arg = 5);
                  
                  int glob_int = 5;
                  
                  foo(); // ОК эквивалентно foo(NULL,5)
                  foo(&glob_int); // OK, эквивалентно foo(&glob_int, 5)
                  foo(13); // BAD - мы попытались привести int к указателю, вместо вызова foo(NULL, 13)
                  

                    0
                    А если опциональному позиционному параметру задать дефолтный конструктор (без параметра), то проблема остается?
                      0
                      Да. А даже если бы это было не так, то решение работало бы только в случае параметров разных типов, что тоже не очень хорошо.
                +2
                Кажется, что если что-то не устраивает в языке для конкретной задачи — просто поменяйте язык. Вариантов много. Если возможности поменять нет (легаси и всё такое) — просто постарайтесь принять это как данное, а для другой задачи выберите что-то более подходящее. Воистину: если не можете изменить ситуацию — измените отношение к ней.
                  +1
                  Возможности языка хороши когда это чистые родные возможности языка, а не искусственные костыли:)
                    0
                    А что делает создание кастомного типа в ООП костылём? Это какбы и есть родная возможность языка, разве нет?
                    Если вы имели в виду не конкретный пример из статьи — прошу извинить, неправильно понял.
                      0
                      Я не про кастомные типы конечно, а про то с чего начал автор статьи (эмуляция именованных параметров, по ссылкам в начале статьи)
                    –6
                    Скажем, да, в Луа можно присваивать значения сразу нескольким переменным
                    Да и в плюсах обычно можно:
                    x = y = getPosition();
                    
                    Нельзя задавать одно начальное значение для нескольких переменных при конструировании.
                      0
                      Я неудачно описал этот пример. Имелось в виду присвоение двух разных значений. Т.е. там еще функции не ограничены одним возвращаемым значением.
                        +16
                        Для плюсов:
                        std::tie(x, y) = getPosition();
                      +14
                      Да, менять местами параметры такой подход не позволит, но это не так и важно.

                      Почему же? Добавим немного С++ 11:

                      struct width {
                        explicit width(int val) : v{val} {}
                        int v;
                      };
                      
                      struct height {
                        explicit height(int val) : v{val} {}
                        int v;
                      };
                      
                      template <typename... T> int getVolume(T... args) {
                        auto t = make_tuple(args...);
                      
                        int w = get_element_by_type<width>(t).v;
                        int h = get_element_by_type<height>(t).v;
                      
                        return w * h * h;
                      }
                      
                      int _tmain(int argc, _TCHAR *argv[]) {
                        cout << getVolume(height(2), width(1)) << endl;
                        cout << getVolume(width(1), height(2)) << endl;
                      
                        return 0;
                      }
                      


                      Функция get_element_by_type возвращает элемент кортежа по его типу. В С++ 14 функция std::get ведёт себя аналогично.

                      get_element_by_type
                      namespace detail {
                      template <class T, std::size_t N, class... Args>
                      struct get_number_of_element_from_tuple_by_type_impl {
                        static constexpr auto value = N;
                      };
                      
                      template <class T, std::size_t N, class... Args>
                      struct get_number_of_element_from_tuple_by_type_impl<T, N, T, Args...> {
                        static constexpr auto value = N;
                      };
                      
                      template <class T, std::size_t N, class U, class... Args>
                      struct get_number_of_element_from_tuple_by_type_impl<T, N, U, Args...> {
                        static constexpr auto value =
                            get_number_of_element_from_tuple_by_type_impl<T, N + 1, Args...>::value;
                      };
                      
                      } // namespace detail
                      
                      template <class T, class... Args>
                      T get_element_by_type(const std::tuple<Args...> &t) {
                        return std::get<detail::get_number_of_element_from_tuple_by_type_impl<
                            T, 0, Args...>::value>(t);
                      }
                      
                        0
                        Мне вот интересно: А это всё compile-time решения? Т.е. не разрастётся ли скомпилированный код от этих наворотов. Второй вопрос — обёртывание базового типа int в структуру с методом добавит нам указатели на методы или нет?
                        Я не специалист в C++, но мне кажется что вся эта обвеска сильно утяжелит передачу параметров (больше места в стеке) и т.д.
                          +1
                          всё в компайлтайме развернется, не бойтесь. Естественно если врубить оптимизацию. Хотя на деле можно было вообще без тупла обойтись и собиралось бы быстрее.
                            +1
                            Таки да, можно обойтись без кортежа. Также можно добавить поддержку параметров по умолчанию.

                            Как-то так:
                            #include "stdafx.h"
                            #include <iostream>
                            using namespace std;
                            
                            namespace detail {
                            template <typename T, typename H, typename... Args>
                            typename enable_if<!is_same<T, H>::value, T>::type
                            get_by_type_impl(H head, Args... tail) { return get_by_type_impl<T>(tail...); }
                            
                            template <typename T, typename... Args>
                            T get_by_type_impl(T head, Args... tail) { return head; }
                            
                            template <typename T, typename... Args> T get_by_type_impl() { return T(); }
                            } // namespace detail
                            
                            template <typename T, typename... Args> T get_arg(Args... tail) {
                              return detail::get_by_type_impl<T>(tail...);
                            }
                            
                            template <typename T> struct arg {
                              explicit arg(T val) : v{val} {}
                              T v;
                              operator T() { return v; }
                            };
                            
                            struct width : arg<int> {
                              using arg::arg;
                            };
                            
                            struct height : arg<int> {
                              explicit height(int i = 42) : arg(i) {}
                            };
                            
                            template <typename... T> int getVolume(T... args) {
                              int w = get_arg<width>(args...);
                              int h = get_arg<height>(args...);
                            
                              return w * h * h;
                            }
                            
                            int _tmain(int argc, _TCHAR *argv[]) {
                              cout << getVolume(width(10)) << endl;
                              cout << getVolume(height(20), width(1)) << endl;
                              cout << getVolume(width(1), height(2)) << endl;
                            
                              return 0;
                            }
                            


                            PS. Я не гуру шаблонного метапрограммирования, так что мог что-то не учесть в данном примере.
                              +3
                              Можно и без с++11 решить вполне поставленную задачу про перемену мест слагаемых в дате:

                              class Date
                              {
                              public:
                                  void set(const Day& d);
                                  void set(const Month& m);
                                  void set(const Year& y);
                              };
                              
                              
                              template<class D, class M, class Y>
                              Date createDate(const D& d, const M& m, const Y& y)
                              {
                                  Date date;
                                  date.set(d);
                                  date.set(m);
                                  date.set(y);
                              
                                  return date;
                              }
                              
                              //теперь можно вызывать
                              createDate(Day(3), Year(4), Month(55));
                              createDate(Year(4), Month(55), Day(3));
                              createDate(Month(55), Day(3), Year(4));
                              
                                +3
                                Это, кстати, поинтересней вышеприведенного решения, т.к. здесь не требуется, чтобы параметры были точно типов Day, Year и Month, а достаточно, чтобы можно было однозначно конвертировать каждый аргумент в один из этих типов. Надо бы только защититься от подобных вызовов:
                                createDate(Day(3), Day(3), Day(3));
                                
                                  +3
                                  И это решаемо:
                                  #include <type_traits>
                                  
                                  template<class D, class M, class Y>
                                  Date createDate(const D& d, const M& m, const Y& y)
                                  {
                                      static_assert(!std::is_same<D, M>::value, "types are equal");
                                      static_assert(!std::is_same<D, Y>::value, "types are equal");
                                      static_assert(!std::is_same<M, Y>::value, "types are equal");
                                      Date date;
                                      date.set(d);
                                      date.set(m);
                                      date.set(y);
                                  
                                      return date;
                                  }
                                  
                                    +1
                                    Это не решает проблему до конца. Пусть у нас есть какой-нибудь тип, который может быть преобразован, скажем, в Day:
                                    struct T {
                                        operator Day() const;
                                    };
                                    

                                    Тогда мы можем делать так:
                                    createDate(T{}, Year(4), Month(55));
                                    
                                    и это реально здорово и является преимуществом вашего метода перед предложенным выше.

                                    Но мы cможем сделать и вот так:
                                    createDate(T{}, Day(4), Month(55));
                                    
                                    поэтому до конца проблема не решена.

                                    P.S. И это уже не «без С++11» ;-)
                                      0
                                      Можно написать простенький CanCast для этого
                                      template<class TO, class FROM>
                                      struct CanCast
                                      {
                                      	static char Can(TO*);
                                      	static char Can(TO);
                                      	static long Can(...);
                                      
                                      	enum
                                      	{
                                      		Result = sizeof(Can(*(FROM*)0)) == sizeof(char) || sizeof(Can((FROM*)0)) == sizeof(char),
                                      	};
                                      };
                                      

                                      и всё будет хорошо.
                            +3
                            Т.е. не разрастётся ли скомпилированный код от этих наворотов.
                            Нет, компилятор соптимизирует весь этот код до печати двух четвёрок, можете в этом сами убедиться, посмотрев ассемблерный код (я использовал printf() для печати, чтобы листинг был покороче и попонятней). Хотя теоретически в этом коде куча дополнительных вызовов мелких вспомогательных функций (можете поставить опцию "-O0" вместо "-O2" и сами посмотреть), весь дизайн С++ построен на том, что компилятор сумеет такие вызовы заинлайнить и соптимизировать получившийся код.
                            Второй вопрос — обёртывание базового типа int в структуру с методом добавит нам указатели на методы или нет?
                            Нет, указатели добавляют только виртуальные методы, эти структуры в точности совпадают с сишной структурой с одним int'ом внутри.
                              0
                              В какое удивительное время мы живем…
                                0
                                так он соптимизировал ваш конкретно код в котором весь этот обвес не нужен ибо начальные и конечные данные никак не меняются. Соответвено он просто выбросил все оставив только необходимое. А если с данными внутри созданной структуры поработать извнне то думаю весь обвес останется.
                                  0
                                  Не уверен, что я правильно вас понял, но в данном примере обвес строится вокруг функции createDate(), а не каких-то структур данных, а типы width и height, если вы о них, — это обычные сишные структуры с одним интом внутри, которые, в свою очередь, на машинном уровне то же самое, что и простой int.
                              0
                              Мне кажется, или теперь метод getVolume принимает вообще любой тип данных?
                              +20
                              А поддерживал бы С++ инициализацию в стиле С99, можно было бы написать так:
                              typedef struct Date {
                                  int day;
                                  int month;
                                  int year;
                              } Date;
                              
                              void f(Date d);
                              ...
                                  f((Date){.day = 1, .month = 2, .year = 3});
                              
                                +3
                                Это, конечно, в стиле Java, но если вы действительно часто встречаетесь с такой проблемой в разных сложных случаях — стоит попробовать шаблон проектирования Builder:

                                Date date = Date.getBuilder().setDay(3).setMonth("Jan").setYear(1999).build();
                                  +2
                                  Вообще подход «Текучий интерфейс» как таковой и переход от функций к функциональным объектам это неплохая альтернатива именованным параметрам (Fluent_interface). Да только вот объявление всего этого гораздо многословнее выходит
                                  –3
                                  Нет, в c++ приходится писать что-то вроде setArmy(-1, -1, -1, 42) вместо православного setArmy(solderTypeThree = 42). Тут именованные параметры пригодились бы. Я уж и не говорю про пары getX/getY вместо getPos в каждом втором граф движке. А, c++, я напомню, стандарт де-факто гдля геймдева.
                                    +5
                                    Если так необходимо сохранить читаемость кода, не лучше и проще ли для тех волшебных значений объявить переменные/константы с соответствующими именами, и их уже отдать в функцию?
                                      +5
                                      Вы давайте тут своими простыми решениями не пудрите нам мозги, у нас тут C++!
                                      +4
                                      Проблема надуманна.
                                      Всегда можно воспользоваться инструментами IDE и получить подсказку, какое имя у параметра, которые в данный момент заполняется.
                                      Разве что вы пишете C++ код в блокноте. Но это не проблем С++ и отсутствия именования переменных.

                                      Вообще, решение отличное! Но только проблемы нету.

                                      someMethod(user1, user2);

                                      Если же объекты неравнозначны, например, метод ставит в подчинение user2 объекту user1, то совсем нелишними будут специальные типы, отражающие роли объектов.

                                      Совсем не лишним будет ООП
                                      user1->someMethod(user2);
                                        +4
                                        Вот когда пишете код, тогда да, тогда и подсказка вылезет, и прочие прелести современных IDE. А вот когда вы код читаете, то замучаетесь мышкой елозить по именам функций, чтобы в подсказке на имена параметров посмотреть.
                                          +3
                                          Не согласен. Ситуации, когда из метода не понятно что у него за параметры — крайне редка.
                                          Давайте уж говорит откровенно, это — говнокод:
                                          Date theDate = createDate(2, 3, 4);

                                          В реале будет что-то вроде:
                                          Date theDate = createDate(currentYear, currentMonth, currentDay);

                                          Или что-то аналогичное. Цифры вообще в коде встречаются крайне редко. В 99% случаев вместо цифр будет либо константа, либо переменная.
                                          А вот такое:
                                          Date theDate = createDate(Year(currentYear), Month(currentMonth), Day(currentDay));

                                          Читабельности не помогает ну вообще никак, и даже наоборот.

                                          Вот пример типичного(ИМХО) кода. Согласитесь, без именования параметров в каждой строке понятно что передается и зачем:
                                          cLabel* Label = InitWindow(cLabel,panel);
                                          Label->setLeft(0);
                                          Label->setTop(0);
                                          Label->setWidth(panel->width());
                                          Label->setHeight(panel->height());
                                          Label->setVAlign(VA_CENTER);
                                          Label->setHAlign(HA_CENTER);
                                          Label->setCaption(Text);
                                          Label->setColor(cColorf::WHITE);
                                          Label->setFont(«Roboto-Regular.ttf»,fontSize);

                                          Даже не типичный макрос InitWindow разгадывается без особых проблем.
                                          Если хочется «короче» — про builder здесь уже говорили.

                                          По сути почти все примеры, в которых наименование действительно нужно будут весьма ограничены и createDate — чуть ли не самый лучший из них.
                                            0
                                            Окей, достаточно выдуманная, но модельная ситуация: завтра в проект приходит тимлид-самодур из США и требует, чтобы createDate принимало аргументы в порядке месяц/день/год. Как упомянутое именование переменных поможет в автоматическом режиме найти все несоответствующие места после смены порядка аргументов и крепко спать ночью после рефакторинга, не беспокоясь, что где-то всё-таки забыл поменять?

                                            Компилятор заставит писать правильные вещи сильнее любых соглашений (если язык достаточно строг, конечно).

                                            Код писать легко. Сложно его читать и поддерживать.
                                              –1
                                              А типы то как помогут?

                                              есть некий наследуемый метод объявленный традиционно:
                                              virtual void classA::createDate(Year year, Month month, Day Day);

                                              и наследники его во всю наследуют:
                                              virtual void classB::createDate(Year year, Month month, Day Day);


                                              Приходит тим лид, и в классе родители параметры меняются местами:
                                              virtual void classA::createDate(Month month, Day Day, Year year);

                                              а в наследниках мы изменить забыли:
                                              virtual void classB::createDate(Year year, Month month, Day Day);

                                              Разве поможет здесь именование параметров?
                                                +1
                                                Поможет ключевое слово override.

                                                И вообще
                                                Моя исключительно субъективная практика показывает, что виртуальные функции должны быть либо чистыми, либо final, остальное в 99.72% случаев не лучший дизайн. Так что в коде выше, если в classA эта функция чистая виртуальная, компилятор и так ругнётся.
                                                  –4
                                                  Это вроде бы С++11, которого много где еще нет.
                                                    +2
                                                    Да, C++11.

                                                    В gcc 4.8.1 реализован полностью, дата выхода — 31 мая 2013 года, больше, чем полтора года назад.
                                                    В clang 3.3 реализован полностью, дата выхода — где-то между 18 июня и 19 апреля 2013 года. Ну тоже полтора года, то есть.

                                                    Конкретно override вообще появился чуть ли не три года назад в gcc.

                                                    В месте, где я сейчас работаю, например, в основном редхат 6.2, с доступным gcc 4.8.1, gcc 4.9.2 и clang 3.4.

                                                    Какое-то странное «много где еще нет», в общем. Вот C++14 ещё много где нет, это точно.
                                                      +1
                                                      Windows Compact 7. Вполне себе актуальная ОС в Enterprise секторе. Разработка только на 2008 студии, например.
                                                      Многие проекты компилируются специально на старых сборках компиляторов, чтобы не дай бог чего не сломалось.
                                                      Плюс большую часть готового кода никто не будет переводить под новые стандарты.
                                                      Так то я с вами согласен, override прекрасно решает эту проблему. Если только нет выше перечисленного.
                                                        +1
                                                        Сочувствую невозможности использовать свежие версии стандартов. Трудно воздержаться от холивара на тему!

                                                        Плюс большую часть готового кода никто не будет переводить под новые стандарты.

                                                        Чего там переводить-то, всё и так вполне себе работает, знай себе допиши -std=c++0x. Даже override и final сформулировали так, чтобы они в контекстах, отличных от объявления функции-члена, не были ключевыми словами.
                                                          +1
                                                          Вы меня заставили задуматься.
                                                          Ведь ничего не мешает при сборке под 2008 студию объявить дефайны
                                                          #define override
                                                          #define final
                                                          и в итоге код написанный с учетом этих директив соберется и под старыми компиляторами. При этом под новыми компиляторами можно будет пользоваться полными возможностями компилятора для отлова ошибок этого вида.
                                                            +2
                                                            Именно так я и посоветовал сделать мейнтейнеру своей софтины, когда он ещё поддерживал её под дистрибутив со старым gcc.

                                                            Кстати, отдельно стоит отметить, что использование final оставляет больше возможностей для девиртуализации и статического анализа, например.
                                                        +1
                                                        Если в вашей области C++11 всеми поддерживается, то это не значит, что он поддерживается везде. Возьмите любой embedded или, хотя бы, тот же Android, и увидите, что ваша область — скорее исключение, чем правило.Clang вообще поддерживается только на некоторых Unix системах, почему вы тогда не рассмотрите тот же MSVC? Мыслите шире.
                                                      +1
                                                      Причем final ещё и оптимизировать помогает, скажем, в таком коде:
                                                      struct A { virtual void f(); }
                                                      struct B : public A { virtual void f() override final; }
                                                      struct C final : public A { virtual void f() override; }
                                                      
                                                      void do_with_B(B& b) { b.f(); }
                                                      void do_with_C(C& c) { c.f(); }
                                                      
                                                      в обоих функциях do_with... вызов f() будет вызвана напрямую соответствующая функция, тогда как без final там был бы вызов через таблицу виртуальных функций.
                                                    –1
                                                    Менять порядок аргументов должна IDE, которая сама найдёт все вызовы, и переставит аргументы соответствующим образов.
                                                    +1
                                                    Почему сразу говнокод. Просто очищеный от всего лишнего пример. Именованные переменные вообще не помогут, потому что в коде может оказаться и так

                                                    Date theDate = createDate(currentYear, currentMonth, currentDay);

                                                    и так

                                                    Date theDate = createDate(currentDay, currentMonth, currentYear);

                                                    и даже так (неопрятная копипаста, почитав блог pvs-studio, можно увидеть, до какой степени ошибки из-за копипасты реальны)

                                                    Date theDate = createDate(currentDay, currentDay, currentYear);

                                                    Все это легко пропустить при чтении кода, да и компилятор доволен. Речь ведь не о том, как «разгадывать» сигнатуры функций, а как обезопасить себя от потенциальных ошибок, переложив максимум рутины на компилятор.
                                                      0
                                                      Ну ведь это и не спасет от ошибок.
                                                      Просто будет написано вот так:
                                                      Date theDate = createDate(Day(currentDay), Month(currentDay), Year(currentYear));
                                                        +1
                                                        Month(currentDay) уже лучше видно.

                                                        Ну а от вообще всех ошибок, конечно, спастись нельзя. Что не мешает искать способы переложить проверку части из них на компилятор.
                                                      +2
                                                      Date theDate = createDate(Year(currentYear), Month(currentMonth), Day(currentDay));

                                                      Лол. Переменная currentYear должна быть типа Year, переменная currentMonth типа Month. И тогда оп-па и код нормально выглядит. Я не знаком со спецификой разработки на C++, но создание экземпляров таких спец.типов с легкостью локализируется в небольшом количестве кода, а дальше — уже просто тупая передача данных между методами. Так что ваш аргумент принимается, но нивелируется.
                                                  0
                                                  Увы. Идея «а давайте обернём примитивные типы» не работает.
                                                  Оно конечно можно пару тройку ключевых типов обернуть чем-то вроде:
                                                  #define BOOST_STRONG_TYPEDEF()
                                                  Но это не айс. Правильная эмуляция примитивного int к, примеру, не примитивная задача.
                                                  Класс это класс, даже если притворяется примитивным типом.
                                                  Как минимум, после компиляции, в релизе и отладке эти ухищрения лучше обратно заменить на нативные типы.

                                                  т.е. хотелось бы строгих типов но нету.

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

                                                      остановился на компромиссе:
                                                      typedef яблоки int;
                                                      typedef груши int;

                                                      груши f = 1;
                                                      яблоки a= 2;
                                                      vector<груши> g;

                                                        +2
                                                        С C++11 можно сделать так:
                                                        enum class apples : unsigned int {};
                                                        
                                                        apples будет отдельным типом (в него нельзя будет присвоить int или груши, скажем), при этом по умолчанию для него будут определены все операторы сравнения и функция хеширования (поэтому, например, его можно положить в любой STL контейнер, ничего дополнительно не дописывая). Из минусов: не работают арифметические операции, не работает вывод через потоки (и то, и то можно ручками дописать) плюс проблемы с синтаксисом конструирования, т.к. единственный способ сконструировать такой тип из встроенного числового типа — это сделать явный каст:
                                                        apples a(0); // Compilation error
                                                        apples b( apples(0) ); // OK
                                                        apples c = apples{0}; // Compilation error
                                                        apples d = apples(0); // OK
                                                        
                                                          0
                                                          можно. для хендлов будет отлично.
                                                          Из минусов: не работают арифметические операции

                                                          это не минус — это приговор, для арифметических типов.
                                                            0
                                                            «Не работают» значит «не работают сразу из коробки». Но ручками все реализуемо:
                                                            apples operator+(apples a, apples b)
                                                            {
                                                                return apples(unsigned(a) + unsigned(b));
                                                            }
                                                            
                                                          0
                                                          А надо ли много копипастить? Например, есть ключ сущности постов (PostKey: EntityKey) или ключ сущности комментариев (CommentKey: EntityKey). В итоге нужно научить корректно с этим работать десериализаторы/сериализаторы, дебагер, биндеры (это специфично для веба — штуки, которые умееют из данных http-запроса строить специфичные модели, чтобы не работать с просто строками). + Научить операциям неявного приведения int -> PostKey; явного приведения PostKey -> int; определить корректные сравнения на равность. Создание экземпляров таких сущностей локализуются в каких-то определенных местах; явное использование сырых значений тоже локализуется в каких-то определенных местах.

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

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

                                                          Рекомендую присмотреться внимательно к данному подходу. Именованые параметры, кстати, эту проблему решают не во всех местах.
                                                            0
                                                            Но есть некоторые товарищи,

                                                            не подскажете ссылочку для ознакомения?

                                                            достаточно найти парочку основных сущностей

                                                            Это реально. Я поискал в своих проектах:
                                                            Заголовок спойлера
                                                            /**внутренний глобальный идентификатор клиента*/
                                                            #ifdef _DEBUG
                                                            BOOST_STRONG_TYPEDEF(DWORD, client_handle_type);
                                                            BOOST_CLASS_IMPLEMENTATION(client_handle_type, primitive_type)
                                                            namespace boost 
                                                            {
                                                                template <>
                                                                struct is_integral<client_handle_type> : public ::boost::true_type {};
                                                                template <>
                                                                struct is_unsigned<client_handle_type> : public ::boost::true_type {};
                                                                template <>
                                                                struct is_signed<client_handle_type> : public ::boost::false_type {};
                                                                template <>
                                                                struct is_arithmetic<client_handle_type> : public ::boost::true_type {};
                                                                template <>
                                                                inline ::std::string lexical_cast<::std::string, client_handle_type>(const client_handle_type & arg)
                                                                {
                                                                    return lexical_cast<::std::string, DWORD>((DWORD)arg);
                                                                }
                                                            }
                                                            #else
                                                            typedef DWORD  client_handle_type;
                                                            #endif
                                                            


                                                            На парочку троечку подобных обёрток меня хватило
                                                      +2
                                                      Погодите. Как альтернатива — можно использовать паттерн Builder с Fluent Interface. Но быть может это оверкил/обходной путь, т.к задача Builder совсем другая. Но все же. Что вы думаете?
                                                        0
                                                        О, прошу прощения, только заметил комментарий выше на эту тему.
                                                        0
                                                        По моему, озвученная проблема имеет место, но ее решение крайне переусложнено. На мой скромный взгляд, конструкция вида
                                                        Date theDate = createDate(2, 3, 4);
                                                        

                                                        вообще не должна появляться в коде. В худшем случае это должно выглядеть примерно так:
                                                        const int kDay = 2;
                                                        const int kYear = 3;
                                                        const int kMonth = 4;
                                                        Date theDate = createDate(kDay, kYear, kMonth);
                                                        
                                                          0
                                                          Имя — это совсем не произвольная строка. Хотя бы потому, что выбирается из заранее известного множества. Еще оно, скажем, не содержит цифр (хотя тут я не уверен).

                                                          Кстати, чудовищное заблуждением. Именем может быть практически любая последовательность Юникод символов. Фамилией — тоже. К тому же понятие фамилия не во всех культурах существует.
                                                            0
                                                            Что-то мне кажется, все предложенные варианты по добавлению функциональности «именованные параметры в C++» — это оверкиллы. Если параметров много и действительно можно запутаться в том, какое значение какому параметру предоставлять или есть еще куча каких-то optional-параметров, то можно воспользоваться Builder + Fluent interface. Ели параметров мало, но все еще можно запутаться, то быть может имеет смысл документировать метод относительно его параметров.
                                                              0
                                                              Насчет интернациональности команды — если разработчик видит выражение:

                                                              Date date = new Date(1, 2, 3);

                                                              то он скорее всего непременно обратится к документации, так как знает, что если результат выполнения некоторого кода можно трактовать двояко, то лучше обратиться к документации. Там все аспекты работы этого кода должны быть описаны.
                                                              0
                                                              Очень крутая парадигма.
                                                              И знаете где её очень не хватает? В OpenCV! Там часто есть риск перепутать координаты, когда обращаемся к элементам матрицы: где-то (row, col), где-то (x, y) — путаница!
                                                                +1
                                                                Веселуха начинается, когда на сцене появляется физика.
                                                                Ну типа там паскали это ньютоны на метр квадратный. И вам нужно описать весь набор допустимых арифметических операций, ну типа

                                                                Pascals operator/ (Newtons force, Squaremeters square) {return Pascals(force.m_value/square.m_value)}


                                                                ЧСХ, сами физики обычно кладут на типобезопасность болт и обезразмеривают все величины. И потом с чистой совестью складывают безразмерные длины с безразмерными температурами.
                                                                  0
                                                                  boost units делает похожее, правда я сам его не использовал

                                                                  www.boost.org/doc/libs/1_57_0/doc/html/boost_units/Examples.html
                                                                    +2
                                                                    Переопределять все операторы для всех возможных единиц измерения не нужно, для этого есть шаблоны. Например, если у нас 3 независимых величины — время, расстояние и масса, то все остальные будут производными.
                                                                    template<int Time, int Dist, int Mass>
                                                                    class Quantity
                                                                    {
                                                                    public:
                                                                        double value;
                                                                    
                                                                        explicit Quantity(double a)
                                                                        {
                                                                            value = a;
                                                                        }
                                                                    
                                                                        // складывать можно только значения одинаковых размерностей
                                                                        Quantity<Time, Dist, Mass> operator+(Quantity<Time, Dist, Mass> b)
                                                                        { return Quantity<Time, Dist, Mass>(this->value + b.value); }
                                                                    
                                                                        // вычитать можно только значения одинаковых размерностей
                                                                        Quantity<Time, Dist, Mass> operator-(Quantity<Time, Dist, Mass> b)
                                                                        { return  Quantity<Time, Dist, Mass>(this->value - b.value); }
                                                                    
                                                                        // при умножении размерности складывются
                                                                        template<int Time2, int Dist2, int Mass2>
                                                                        Quantity<Time + Time2, Dist + Dist2, Mass + Mass2> operator*(Quantity<Time2, Dist2, Mass2> b)
                                                                        { return Quantity<Time + Time2, Dist + Dist2, Mass + Mass2>(this->value * b.value); }
                                                                    
                                                                        // при делении размерности вычитаются
                                                                        template<int Time2, int Dist2, int Mass2>
                                                                        Quantity<Time - Time2, Dist - Dist2, Mass - Mass2> operator/(Quantity<Time2, Dist2, Mass2> b)
                                                                        { return Quantity<Time - Time2, Dist - Dist2, Mass - Mass2>(this->value / b.value); }
                                                                    };
                                                                    
                                                                    typedef Quantity<1, 0, 0> second;
                                                                    typedef Quantity<0, 1, 0> meter;
                                                                    typedef Quantity<0, 0, 1> kilogram;
                                                                    
                                                                    typedef Quantity<0, 2, 0> square_meter;
                                                                    typedef Quantity<0, 3, 0> cubic_meter;
                                                                    typedef Quantity<-2, 1, 1> newton; // kg*m/s2
                                                                    typedef Quantity<-2, -1, 1> pascal; // kg/(m*s2)
                                                                    Это конечно тоже не самый лучший способ, если потом появится ещё одна независимая величина придётся переписывать класс Quantity, если заморочиться можно сделать всё в общем виде, но создатели библиотеки boost units похоже уже заморочились, я её не использовал, не знаю на сколько она удобна.
                                                                      0
                                                                      Хотел написать этот коммент. Для инженера boost::units просто супер. Куча ошибок и опечаток стала вылезать во время компиляции. Хотя нужно привыкнуть.
                                                                      0
                                                                      Кстати, в F# компилятор умеет работать с единицами измерения. Например:

                                                                      [<Measure>] type m
                                                                      [<Measure>] type s
                                                                      
                                                                      let distance = 100<m>
                                                                      let time = 5<s>
                                                                      let speed = distance / time // Размерность будет m/s
                                                                      

                                                                      Правда, этот подход не убережет от странных делений, типа «доллар на секунду в квадрате», но зато не даст случайно сложить или сравнить килограммы с галлонами.
                                                                        0
                                                                        Доллар на секунду в квадрате — это то, с какой скоростью изменяется цена на что-то.
                                                                          0
                                                                          Скорее производная от изменения цены. В финансовых приложениях, возможно, и такое бывает. Я просто хотел показать пример маловероятной размерности.
                                                                      +1
                                                                      > Тут проще создать тип Position и присваивать значение его объекту

                                                                      И сколько ж таких типов выйдет, интересно, скажем в проекте на миллион строк? Тысяча? Десять тысяч?
                                                                      Если над проектом работает более одного человека — к гадалке не ходи, в разных концах проекта будут заведены разные типы для одного и того же. Вот веселье-то.
                                                                      А ведь их все ещё продокументировать надо. Сколько страниц будет в документации к проекту?

                                                                      Чудес в мире не бывает. За всё надо платить.
                                                                        0
                                                                        Каких таких? Да, всегда можно выкрутиться и не заводить новый тип. Но в проекте на миллион строк с более чем одним разработчиком, которые творят что хотят, каждый выкрутится по-своему… Даже разнообразие вариантов в комментариях к статье намекает. И документации это добавит не только объема, но и разнообразия.
                                                                        Если вы имели в виду «таких простых», так это потому что пример. Даже банальная позиция со временем обрастает всякими полезными методами типа серилизации, отладочного вывода, проверки корректности. В проектах до сотни тысяч строк не разу не видел, чтобы какой-то тип висел просто сборником полей без дополнительной логики, которую иначе пришлось бы копипастить в сотни мест…
                                                                        0
                                                                        Самое время привести основную мысль статьи. Примитивным стандартным типам не место в интерфейсах реальной системы. Такие типы — просто строительные блоки, из которых нужно именно строить, а не пытаться жить внутри.

                                                                        Да, и в Аде с этим (созданием пользовательских типов на базе примитивных типов) все значительно лучше чем в С++, поэтому они там повсеместно используются, и базовых типов обычно действительно в интерфейсах пакетов не видно. Просто потому, что создать свой тип — это одна-две строчки. Ну и вообще, не принято там использовать базовый Integer какой-нибудь.

                                                                        И это то, чего в С++ действительно не хватает. Хотя можно конечно извратиться, и попробовать сделать аналог на шаблонах. Будет более громоздко конечно, но хоть как-то.
                                                                          0
                                                                          Редко комментирую посты, но просто не могу сдержаться. Я иногда чувствую себя инопланетянином.

                                                                          Чтобы не спутать порядок параметров при вызове функции вы готовы написать три новых класса? Правда?!… Серьезно?!!! Вы собираетесь это делать для каждой функции с однотипными параметрами?

                                                                          Вы понимаете, зачем нужен язык программирования — он нужен для того, чтобы точно и УДОБНО выражать мысли программиста. Если для такой простой задачи (которая в С++, кстати говоря, обычно решается изменением названия функции на createDate_DayMonthYear), если вместо этого вам нужно создавать три новых класса — это НЕ УДОБНО. Это ОЧЕНЬ ПЛОХО. Это грубое нарушение принципа KISS. В том числе потому, что вероятность ошибки при написании 3 новых классов и модификации исходной функции createDate в разы выше, чем вероятность спутать порядок параметров.
                                                                            0
                                                                            Чего ж инопланетянином. Как раз описанные вами подходы более распространены, по крайней мере, по моим наблюдениям. А по поводу KISS — это когда как. Если createDate используется нечасто, то, возможно, создание специальных типов будет перегибом. Проще назвать createDate_DayMonthYear и проверять корректность вручную. Хотя я тут ничего, кроме лабораторной работы, предствить не могу. В противном случае проверку можно переложить на компилятор, сэкономив кучу времени и нервов.
                                                                              0
                                                                              В том числе потому, что вероятность ошибки при написании 3 новых классов
                                                                              Нам же не с нуля надо эти классы разрабатывать с какой-то уникальной функциональностью в каждом, они весьма однотипны. Всё можно устроить так, что добавление нового класса будет требовать от нас написание одной строчки кода (например, завернуть объявление класса в макрос) или одного клика в IDE для вставки соответствующего сниппета.
                                                                                +2
                                                                                > createDate_DayMonthYear

                                                                                createPerson_FirstName_LastName_DateOfBirthDayMonthYear_Gender()

                                                                              Only users with full accounts can post comments. Log in, please.