Приспадане на тип шаблон за манипулатори на поток

Не съм сигурен дали този код няма да се компилира.

Примерният код, с който работя:

#include <iostream>
using std::cout;
using std::endl;

class Foo {
    public:
        template<typename T>
        Foo& operator<<(const T& t) {
            cout << t;
            return *this;
        }
};

int main() {
    Foo foo;
    foo << "Hello World"; // perfectly fine
    foo << endl; // shit hits the fan

    return 0;
}

Това е грешката:

test.cpp:19:12: error: no match for ‘operator<<’ in ‘foo << std::endl’
test.cpp:19:12: note: candidates are:
test.cpp:10:14: note: template<class T> Foo& Foo::operator<<(const T&)
test.cpp:10:14: note:   template argument deduction/substitution failed:
test.cpp:19:12: note:   couldn't deduce template parameter ‘T’

Объркан съм защо не може да замести типа на функцията endl (ostream& (*)(ostream&)) за T, където очевидно е добре да го прави, когато посочите cout << endl;

Намирам за допълнително озадачаващо, че това решава проблема [редактирано]

Foo& operator<<(ostream& (*f)(ostream&)) {
    cout << f;
    return *this;
}

В случай, че въпросът не е ясен, питам защо не може да изведе шаблона на първо място.


person Anthony Sottile    schedule 23.02.2013    source източник
comment
Какво се опитваш да постигнеш? Някаква конкретна причина да не извличате от std::basic_ostream<C,CT> или да прилагате streambuf?   -  person sehe    schedule 23.02.2013
comment
Към последния въпрос: Първоначално не можеше да изведе шаблона, тъй като множество претоварвания бяха приложими - следователно случаят с референтна функция беше двусмислен.   -  person sehe    schedule 23.02.2013


Отговори (2)


endl е манипулатор, т.е. това е неразрешен тип функция. Има няколко претоварвания и приспадането на типа не може да реши кое искате.

По-конкретно, ето как изглежда endl (в GNU libc++):

/**
 *  @brief  Write a newline and flush the stream.
 *
 *  This manipulator is often mistakenly used when a simple newline is
 *  desired, leading to poor buffering performance.  See
 *  http://gcc.gnu.org/onlinedocs/libstdc++/manual/bk01pt11ch25s02.html
 *  for more on this subject.
*/
template<typename _CharT, typename _Traits>
  inline basic_ostream<_CharT, _Traits>&
  endl(basic_ostream<_CharT, _Traits>& __os)
  { return flush(__os.put(__os.widen('\n'))); }

Актуализирано И така, проблемът е, че компилаторът не може да изведе кой екземпляр на endl ще подадете (това е неразрешено претоварване). Може да заобиколите това, като вместо това направите static_cast<ostream&(*)(ostream&)>(endl).

Разбира се, това не е удобно. Ето една проста корекция: http://liveworkspace.org/code/2F2VHe%241

#include <iostream>
using std::cout;
using std::endl;

class Foo : public std::ostream
{
    public:
        template<typename T>
        Foo& operator<<(T&& t) {
            cout << std::forward<T>(t);
            return *this;
        }

        typedef std::ostream& (manip)(std::ostream&);

        Foo& operator<<(manip& m) {
            cout << m;
            return *this;
        }
};

int main() {
    Foo foo;
    foo << "Hello World"; // perfectly fine
    foo << endl; // everything is fine

    return 0;
}
person sehe    schedule 23.02.2013
comment
Бихте ли обяснили по-подробно, причината е, че всъщност не се опитват различните замествания на претоварване, за да се определи правилното? Или очаквам твърде много от компилатора - person Anthony Sottile; 23.02.2013
comment
@AnthonySottile Предоставих прост пример за това как да се погрижим за манипулатора endl: liveworkspace.org/code/2F2VHe$1 - person sehe; 23.02.2013
comment
Бихте ли разкрасили разликите между вашия код и кода, който имах по-горе, и причините за разликите. Например моето текущо решение прави същото с изрично дефиниране на претоварването за манипулатора -- но можете ли да обясните използването на std::forward. Можете също така да обясните причината защо компилаторът не може да избере претоварването без тази изрична дефиниция. - person Anthony Sottile; 23.02.2013
comment
@AnthonySottile Съжалявам, std::forward е част от перфектното препращане и не е от съществено значение за проблема. Май от силата на навика го написах :) Виж напр. stackoverflow.com/a/5481588/85371 - person sehe; 23.02.2013
comment
Разкрасих обяснението на endl като манипулатор, и отговорих изрично на последния ви въпрос. - person sehe; 23.02.2013
comment
@JohannesSchaub-litb Опа. Това всъщност не го пробвах :). „Поправих“ го, като поисках указател на функция (дали ostream&(&)(ostream&) би било добре?) - person sehe; 23.02.2013

Проблемът е, че endl е манипулатор, дефиниран като функционален шаблон. Параграф 27.7.1 от стандарта C++11 определя неговия подпис:

template <class charT, class traits>
basic_ostream<charT,traits>& endl(basic_ostream<charT,traits>& os);
template <class charT, class traits>

Освен това, съгласно параграф 13.3.1 относно разрешаването на претоварване:

Във всеки случай, когато даден кандидат е шаблон на функция, специализациите на шаблон на кандидат-функция се генерират чрез дедукция на аргумент на шаблон (14.8.3, 14.8.2). След това тези кандидати се обработват като кандидат функции по обичайния начин.

Вашият operator << е дефиниран като шаблон и компилаторът трябва да изведе типа на T. Как обаче компилаторът може да разбере коя инстанция на endl имахте предвид? Как може да изведе аргументите на шаблона charT и traits? Няма нищо друго във вашето обаждане до operator <<, от което да може да се заключи.

Имате два изхода от този проблем. Или прехвърляте типа endl изрично, за да кажете на компилатора кое претоварване да бъде избрано:

foo << (std::ostream& (*)(std::ostream&))endl;

Или, както направихте, създавате претоварване на operator <<, което приема функция с този специфичен подпис. Вашият компилатор сега ще го избере:

Foo& operator<<(ostream& (*f)(ostream&)) 
{
    return *this << f;
}

В тази дефиниция на функция няма неяснота какво е f: нейният тип е точно дефиниран. Тук обаче бъдете внимателни: тази функция вероятно не прави това, което очаквате! Всъщност той просто продължава да се извиква, генерирайки безкрайна рекурсия!

Следователно това твърдение:

[...] забележете, че всъщност извиквам другата си реализация на метода:

Е неправилно: вие не извиквате другата реализация на метода, вие продължавате да извиквате същата функция отново и отново.

person Andy Prowl    schedule 23.02.2013
comment
стреля, добър улов, вероятно трябваше да стартирам кода си, вместо просто да проверявам дали се компилира... - person Anthony Sottile; 23.02.2013