Почему бы не вывести параметр шаблона из конструктора?

103

мой вопрос сегодня довольно прост: почему компилятор не может вывести параметры шаблона из конструкторов классов, так же как он может сделать из параметров функции? Например, почему следующий код не может быть действительным:

template<typename obj>
class Variable {
      obj data;
      public: Variable(obj d)
              {
                   data = d;
              }
};

int main()
{
    int num = 2;
    Variable var(num); //would be equivalent to Variable<int> var(num),
    return 0;          //but actually a compile error
}

Как я уже сказал, я понимаю, что это неверно, поэтому мой вопрос: почему это не так? Может ли это создать серьезные синтаксические дыры? Есть ли случай, когда эта функция не нужна (когда определение типа вызовет проблемы)? Я просто пытаюсь понять логику, позволяющую вывод шаблонов для функций, но не для должным образом сконструированных классов.

GRB
источник
Я бы пригласил кого-нибудь (я делаю это, только не сейчас), чтобы составить ответ Драхакара и Питиса (по крайней мере) в качестве хороших контрпримеров, почему он не может работать
jpinto3912
2
Также обратите внимание, что это легко обойти с помощьюtemplate<class T> Variable<T> make_Variable(T&& p) {return Variable<T>(std::forward<T>(p));}
Mooing Duck
3
Вы можете получить то, что хотите var = Variable <decltype (n)> (n);
QuentinUK
18
C ++ 17 позволит это! Это предложение было принято: open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0091r0.html
underscore_d
1
@underscore_d Отлично! О времени! Мне казалось естественным, что это так, как должно работать, и источником раздражения, что это не так.
amdn

Ответы:

47

Я думаю, что это неверно, потому что конструктор не всегда является единственной точкой входа в класс (я говорю о конструкторе копирования и операторе =). Итак, предположим, вы используете свой класс следующим образом:

MyClass m(string s);
MyClass *pm;
*pm = m;

Я не уверен, будет ли для анализатора настолько очевидным, что тип шаблона является PM MyClass;

Не уверен, что то, что я сказал, имеет смысл, но не стесняйтесь добавлять комментарии, это интересный вопрос.

C ++ 17

Принято, что в C ++ 17 будет вывод типа из аргументов конструктора.

Примеры:

std::pair p(2, 4.5);
std::tuple t(4, 3, 2.5);

Принятые статьи .

Драхакар
источник
8
На самом деле это отличный момент, о котором я никогда не думал. Я не вижу никакого способа обойтись без того факта, что указатель должен быть специфичным для типа (т.е. это должен быть MyClass <string> * pm). В этом случае все, что вам нужно сделать, это избавить себя от указания типа при создании экземпляра; несколько простых символов дополнительной работы (и только если объект сделан в стеке, а не в куче, как указано выше). Я всегда подозревал, что вывод класса может открыть синтаксическую банку червей, и я думаю, что это может быть оно.
GRB,
2
Я не совсем понимаю, как разрешение вывода параметров шаблона из конструкторов потребует разрешения неспециализированных объявлений без вызовов конструкторов, как во второй строке. Т.е. MyClass *pmздесь было бы недействительно по той же причине, что объявленная функция template <typename T> void foo();не может быть вызвана без явной специализации.
Кайл Стрэнд
3
@KyleStrand Да, говоря, что «аргументы шаблона класса не могут быть выведены из их конструкторов, потому что [пример, который не использует никаких конструкторов] », этот ответ совершенно неуместен. Я искренне не могу поверить, что это было принято, достигло +29, потребовалось 6 лет, чтобы кто-то заметил вопиющую проблему, и просидел без единого отрицательного голоса в течение 7 лет. Никто другой не думает, пока они читают, или ...?
underscore_d
1
@underscore_d Мне нравится, что в его нынешнем виде в этом ответе говорится: «С этим предложением могут быть некоторые проблемы; я не уверен, имеет ли смысл то, что я только что сказал (!), не стесняйтесь комментировать (!!); и о, кстати, именно так будет работать C ++ 17 ".
Кайл Стрэнд
1
@KyleStrand А, да, это еще одна проблема, которую я заметил, но забыл упомянуть среди всего прочего. Редактирование C ++ 17 не было внесено OP ... и IMO не должно было быть одобрено, а было опубликовано как новый ответ: его можно было бы отклонить как «изменение значения сообщения», даже если бы сообщение было было бессмысленно начинать ... Я не знал, что редактирование совершенно новых разделов было честной игрой и, конечно, были отклонены менее радикальные правки, но я думаю, это удача с точки зрения того, какие рецензенты вы получите.
underscore_d
27

Вы не можете делать то, о чем просите, по причинам, к которым обратились другие люди, но вы можете сделать это:

template<typename T>
class Variable {
    public: Variable(T d) {}
};
template<typename T>
Variable<T> make_variable(T instance) {
  return Variable<T>(instance);
}

что для всех намерений и целей - это то же самое, о чем вы просите. Если вам нравится инкапсуляция, вы можете сделать make_variable статической функцией-членом. Это то, что люди называют именованным конструктором. Таким образом, он не только делает то, что вы хотите, но и почти вызывает то, что вы хотите: компилятор определяет параметр шаблона из (названного) конструктора.

NB: любой разумный компилятор оптимизирует временный объект, когда вы напишете что-то вроде

auto v = make_variable(instance);
Лайонел
источник
6
Хочу отметить, что в таком случае делать функцию статическим членом не особенно полезно, потому что для этого вам нужно будет указать аргумент шаблона, чтобы класс все равно вызвал его, поэтому не было бы смысла выводить его.
Predelnik 03
3
И даже лучше, в C ++ 11 вы можете сделать auto v = make_variable(instance)так, чтобы вам не приходилось указывать тип
Клаудиу,
1
Да, лол от идеи объявить функцию make в качестве staticчлена ... подумайте об этом чуть меньше секунды. Это в стороне: свободные функции замыкающих были действительно решением, но это много избыточных шаблонным, что в то время как вы печатаете это, вы просто знаете , вы не должны , потому что компилятор имеет доступ ко всей информации вы повторить. ... и, к счастью, C ++ 17 канонизирует это.
underscore_d
21

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

Вывод шаблона-аргумента для шаблонов классов в C ++ 17

Здесь (любезно предоставлено редакцией принятого ответа Олжасом Жумабеком) документ с подробным описанием соответствующих изменений в стандарте.

Решение проблем, связанных с другими ответами

Текущий самый популярный ответ

Этот ответ указывает на то, что «конструктор копирования и operator=» не знает правильных специализаций шаблона.

Это чушь, потому что стандартные конструкторы-копии operator= существуют только для известного типа шаблона:

template <typename T>
class MyClass {
    MyClass(const MyClass&) =default;
    ... etc...
};

// usage example modified from the answer
MyClass m(string("blah blah blah"));
MyClass *pm;   // WHAT IS THIS?
*pm = m;

Здесь, как я отмечал в комментариях, нет причин для того, MyClass *pmчтобы быть законным объявлением с новой формой вывода или без нее : MyClass это не тип (это шаблон), поэтому нет смысла объявлять указатель на тип MyClass. Вот один из возможных способов исправить этот пример:

MyClass m(string("blah blah blah"));
decltype(m) *pm;               // uses type inference!
*pm = m;

Здесь pmон уже имеет правильный тип, поэтому вывод тривиален. Более того, невозможно случайно смешать типы при вызове конструктора копирования:

MyClass m(string("blah blah blah"));
auto pm = &(MyClass(m));

Здесь pmбудет указатель на копию m. Здесь MyClassсоздается копия, mкоторая имеет тип MyClass<string>не несуществующий тип MyClass). Таким образом, в точке , где pmвыводится «S типа, там есть достаточно информации , чтобы знать , что шаблонный тип m, и , следовательно , шаблон-типа pm, являетсяstring .

Более того, следующее всегда вызывает ошибку компиляции :

MyClass s(string("blah blah blah"));
MyClass i(3);
i = s;

Это потому, что объявление конструктора копирования не является шаблоном:

MyClass(const MyClass&);

Здесь шаблон типа копирования-конструктор аргумент соответствует шаблону типа класса в целом; то есть, когда MyClass<string>создается экземпляр, MyClass<string>::MyClass(const MyClass<string>&);создается с ним, а когда MyClass<int>создается экземпляр, MyClass<int>::MyClass(const MyClass<int>&);создается экземпляр. Если это не указано явно или не объявлен шаблонный конструктор, компилятор не имеет причин для создания экземпляраMyClass<int>::MyClass(const MyClass<string>&); , что, очевидно, было бы неуместным.

Ответ Кэтэлина Питиша

Пити приводит пример вывода, Variable<int>а Variable<double>затем заявляет:

У меня в коде указано одно и то же имя типа (Variable) для двух разных типов (Variable и Variable). С моей субъективной точки зрения, это очень сильно влияет на читабельность кода.

Как отмечалось в предыдущем примере, Variableсамо по себе не является именем типа, хотя новая функция делает его синтаксически похожим на имя типа.

Затем Пити спрашивает, что произойдет, если не будет задан конструктор, который позволил бы сделать соответствующий вывод. Ответ состоит в том, что логический вывод не разрешен, потому что вывод запускается вызовом конструктора . Без вызова конструктора нет вывода .

Это похоже на вопрос о том, какая версия fooвыведена здесь:

template <typename T> foo();
foo();

Ответ заключается в том, что этот код является незаконным по указанной причине.

Ответ MSalter

Насколько я могу судить, это единственный ответ, который вызывает законное беспокойство по поводу предлагаемой функции.

Пример такой:

Variable var(num);  // If equivalent to Variable<int> var(num),
Variable var2(var); // Variable<int> or Variable<Variable<int>> ?

Ключевой вопрос заключается в том, выбирает ли компилятор здесь конструктор с выводом типа или конструктор копирования ?

Пробуя код, мы видим, что выбран конструктор копирования. Чтобы развернуть пример :

Variable var(num);          // infering ctor
Variable var2(var);         // copy ctor
Variable var3(move(var));   // move ctor
// Variable var4(Variable(num));     // compiler error

Я не уверен, как это указывается в предложении и новой версии стандарта; Похоже, что это определяется «руководящими принципами дедукции», которые представляют собой новый стандарт, который я еще не понимаю.

Я также не уверен, почему var4удержание незаконно; ошибка компилятора из g ++, похоже, указывает на то, что оператор анализируется как объявление функции.

Кайл Стрэнд
источник
Какой прекрасный, подробный ответ! var4это просто случай "самого неприятного разбора" (не имеющего отношения к выводу аргументов шаблона). Раньше мы просто использовали для этого дополнительные скобки, но в наши дни я думаю, что использование фигурных скобок для однозначного обозначения конструкции - обычный совет.
Sumudu Fernando
@SumuduFernando Спасибо! Вы имеете в виду, что Variable var4(Variable(num));это рассматривается как объявление функции? Если да, то почему Variable(num)это действительная спецификация параметра?
Кайл Стрэнд
@SumuduFernando Неважно, я понятия не имел, что это действительно так: coliru.stacked-crooked.com/a/98c36b8082660941
Кайл Стрэнд
11

По-прежнему отсутствует: это делает следующий код довольно неоднозначным:

int main()
{
    int num = 2;
    Variable var(num);  // If equivalent to Variable<int> var(num),
    Variable var2(var); //Variable<int> or Variable<Variable<int>> ?
}
MSalters
источник
Еще один хороший момент. Предполагая, что существует переменная, определяемая конструктором копирования (Variable <obj> d), должен быть установлен какой-то приоритет.
GRB
1
Или, в качестве альтернативы, пусть компилятор снова выдаст ошибку неопределенного параметра шаблона, как я предлагал в отношении ответа Питиса. Однако, если вы выберете этот путь, количество раз, когда вывод может происходить без проблем (ошибок), становится все меньше и меньше.
GRB
На самом деле это интересный момент, и (как я отметил в своем ответе) я еще не уверен, как принятое предложение C ++ 17 решает эту проблему.
Кайл Стрэнд,
9

Предположим, что компилятор поддерживает то, что вы просили. Тогда этот код действителен:

Variable v1( 10); // Variable<int>

// Some code here

Variable v2( 20.4); // Variable<double>

Теперь у меня есть одно и то же имя типа (Variable) в коде для двух разных типов (Variable и Variable). С моей субъективной точки зрения, это очень сильно влияет на читабельность кода. Наличие одного и того же имени типа для двух разных типов в одном пространстве имен меня вводит в заблуждение.

Позднее обновление: еще одна вещь, на которую следует обратить внимание: частичная (или полная) специализация шаблона.

Что, если я специализируюсь на переменных и не предоставлю конструктора, как вы ожидаете?

Итак, у меня было бы:

template<>
class Variable<int>
{
// Provide default constructor only.
};

Тогда у меня есть код:

Variable v( 10);

Что должен делать компилятор? Использовать определение универсального класса Variable, чтобы сделать вывод, что это Variable, а затем обнаружить, что Variable не предоставляет один конструктор параметров?

Кэтэлин Питиш
источник
1
Хуже того: что, если у вас есть только Variable <int> :: Variable (float)? Теперь у вас есть два способа вывести переменную (1f) и нет способа вывести переменную (1).
MSalters
Это хороший момент, но его можно легко превзойти с помощью кастинга: Variable v1 ((double) 10)
jpinto3912
Я согласен, что читабельность кода - это субъективная проблема, однако я на 100% согласен с тем, что вы говорите о специализации шаблонов. Решением, вероятно, было бы выдать ошибку неопределенного параметра шаблона (как только компилятор смотрит на специализацию <int> и не видит действительных конструкторов, скажите, что он не знает, какой шаблон вы хотите использовать, и что вы должны указать явно), но Я согласен, что это не очень хорошее решение. Я бы добавил это как еще одну серьезную синтаксическую дыру, с которой нужно будет справиться (но ее можно решить, если принять последствия).
GRB,
4
@ jpinto3912 - вы упускаете суть. Компилятор должен создать экземпляры ВСЕХ возможных переменных <T>, чтобы проверить, обеспечивает ли ЛЮБАЯ переменная ctor <T> :: Variable неоднозначный ctor. Проблема не в том, чтобы избавиться от двусмысленности - просто создайте экземпляр переменной <double> самостоятельно, если вы этого хотите. Это прежде всего обнаружение той двусмысленности, которая делает это невозможным.
MSalters
6

Стандарт C ++ 03 и C ++ 11 не позволяет выводить аргумент шаблона из параметров, переданных в конструктор.

Но есть предложение по «Вычету параметров шаблона для конструкторов», так что вы можете вскоре получить то, о чем просите. Изменить: действительно, эта функция была подтверждена для C ++ 17.

См .: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3602.html и http://www.open-std.org/jtc1/sc22/wg21/docs/. документы / 2015 / p0091r0.html

ChetS
источник
Эта функция была добавлена ​​в C ++ 17, но не в том случае, если «скоро» применимо к временным рамкам от 6 до 8 лет. ;)
ChetS
2

Многие классы не зависят от параметров конструктора. Есть только несколько классов, которые имеют только один конструктор и параметризуются на основе типа (ов) этого конструктора.

Если вам действительно нужен вывод шаблона, используйте вспомогательную функцию:

template<typename obj>
class Variable 
{
      obj data;
public: 
      Variable(obj d)
      : data(d)
      { }
};

template<typename obj>
inline Variable<obj> makeVariable(const obj& d)
{
    return Variable<obj>(d);
}
rlbond
источник
1
Конечно, эта функциональность окажется полезной только для некоторых классов, но то же самое можно сказать и о выводе функций. Не все шаблонные функции также берут свои параметры из списка аргументов, но мы разрешаем вывод для тех функций, которые это делают.
GRB,
1

Выведение типов в текущем C ++ ограничено шаблонными функциями, но уже давно стало понятно, что вывод типов в других контекстах был бы очень полезен. Следовательно, C ++ 0x auto.

Хотя именно то , что вы предлагаете, будет невозможно в C ++ 0x, следующее показывает, что вы можете довольно близко подойти:

template <class X>
Variable<typename std::remove_reference<X>::type> MakeVariable(X&& x)
{
    // remove reference required for the case that x is an lvalue
    return Variable<typename std::remove_reference<X>::type>(std::forward(x));
}

void test()
{
    auto v = MakeVariable(2); // v is of type Variable<int>
}
Джеймс Хопкин
источник
0

Вы правы, компилятор может легко догадаться, но, насколько мне известно, этого нет в стандарте или C ++ 0x, поэтому вам придется подождать еще как минимум 10 лет (фиксированная скорость оборачиваемости стандартов ISO), прежде чем поставщики компиляторов добавят эту функцию

Роберт Гулд
источник
Это неверно с будущим стандартом, будет введено ключевое слово auto. Взгляните на пост Джеймса Хопкинса в этой теме. stackoverflow.com/questions/984394/… . Он показывает, как это будет возможно в C ++ 0x.
ovanes
1
Чтобы исправить себя, ключевое слово auto также присутствует в текущем стандарте, но для другой цели.
ovanes
Похоже, это будет 8 лет (с момента этого ответа) ... так что 10 лет было неплохим предположением, даже несмотря на то, что за это время было два стандарта!
Кайл Стрэнд
-1

Давайте посмотрим на проблему применительно к классу, с которым все должны быть знакомы - std :: vector.

Во-первых, очень распространенное использование вектора - использование конструктора, который не принимает параметров:

vector <int> v;

В этом случае, очевидно, невозможно сделать вывод.

Второе распространенное использование - создание вектора предварительно заданного размера:

vector <string> v(100);

Здесь, если был использован вывод:

vector v(100);

мы получаем вектор целых чисел, а не строк, и предположительно он не имеет размера!

Наконец, рассмотрим конструкторы, которые принимают несколько параметров - с «выводом»:

vector v( 100, foobar() );      // foobar is some class

Какой параметр следует использовать для вывода? Нам понадобится какой-то способ сообщить компилятору, что это должен быть второй.

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


источник
3
Я думаю, вы неправильно поняли идею. Вывод типа для конструкторов будет происходить только в том случае, если тип шаблона является частью конструктора. Предположим, что у вектора есть шаблон определения шаблона <typename T>. Ваш пример не является проблемой, потому что конструктор вектора будет определен как вектор (размер int), а не вектор (размер T). Только в случае вектора (размер T) может произойти какой-либо вывод; в первом примере компилятор выдаст ошибку, говоря, что T не определено. По сути идентично тому, как работает вывод шаблона функции.
GRB,
Значит, это будет иметь место только для конструкторов, которые имеют один параметр и где этот параметр является типом параметра шаблона? Это кажется исчезающе малым числом случаев.
Это не обязательно должен быть единственный параметр. Например, у одного может быть конструктор вектора vector (int size, T firstElement). Если шаблон имеет несколько параметров (template <typename T, typename U>), у одного может быть Holder :: Holder (T firstObject, U secondObject). Если шаблон имеет несколько параметров, но конструктор принимает только один из них, например, Holder (U secondObject), то T всегда нужно указывать явно. Правила должны быть максимально похожи на вывод шаблона функции.
GRB
-2

Сделав ctor шаблоном, переменная может иметь только одну форму, но разные ctors:

class Variable {
      obj data; // let the compiler guess
      public:
      template<typename obj>
      Variable(obj d)
       {
           data = d;
       }
};

int main()
{
    int num = 2;
    Variable var(num);  // Variable::data int?

    float num2 = 2.0f;
    Variable var2(num2);  // Variable::data float?
    return 0;         
}

Видеть? Мы не можем иметь несколько членов Variable :: data.

Ник Дандулакис
источник
Это не имело бы смысла ни при каком сценарии. obj с точки зрения данных obj не определен, поскольку этот класс больше не является шаблоном. В любом случае такой код будет недействительным.
GRB
Мне хотелось описанного вами поведения компилятора, поэтому я нашел способ обойти это ограничение (в моем случае), что может вас заинтересовать, stackoverflow.com/questions/228620/garbage-collection-in-c-why/…
Ник Дандулакис
-2

Дополнительную информацию см. В разделе «Вывод аргументов шаблона C ++» .

Игорь Кривоконь
источник
4
Я читал эту статью раньше, и, похоже, она мало говорила о том, что я говорю. Единственный раз, когда автор, кажется, говорит о выводе аргументов в отношении классов, - это когда он говорит, что это невозможно сделать в начале статьи;) - если бы вы могли указать на разделы, которые, по вашему мнению, имеют отношение к классам, я я действительно ценю это.
GRB