Domanda

Let's say I have the following template function:

// #include <iostream>

template< typename T >
T read( std::istream& in )
{
    T x;
    in >> x;  // (could check for failure, but not the point)
    return x;
}

It is intended to be used like this (to initialize const variables from user input):

// #include <string>
// std::istream& in = std::cin;

const int integer = read< int >( in );
const double decimal = read< double >( in );
const std::string word = read< std::string >( in );
...

But note that the same type must be provided twice: once for the variable declaration, and once again for the call. Such a duplication would be better avoided (see also the DRY principle, "Don't Repeat Yourself"), because each change has to be duplicated, and for example code like this:

const int integer = read< double >( in );
const double decimal = read< int >( in );
const std::string word = read< char* >( in );

compiles "fine" but will probably do bad things at runtime (especially the third one, which causes a warning).

Is there a way to avoid the type repetition?


I can already think of two ways, so instead of pretending I don't know them I'm quickly exposing them before asking for more:

  1. Return Type Resolver

    I recently discovered the "Return Type Resolver" idiom (from this question). So we can create a simple class with a template implicit conversion operator around the existing function:

    class Read {
        std::istream& m_in;
    public:
        explicit Read( std::istream& in ) : m_in( in ) { }
    
        template< typename T >
        operator T() const
        {
            return read< T >( m_in );
        }
    };
    

    And we can now write code like this:

    const int integer = Read( in );
    const double decimal = Read( in );
    const std::string word = Read( in );
    ...
    

    where the "return type" is automatically deduced from the variable type (note: the return type is not const even if the variable is, so this is truly equivalent to the original code (especially after the probable inlining)).

  2. C++11 auto

    Starting from C++11 we can use the auto specifier as an alternative, which also avoids naming the type twice, but the approach is "inverse":

    const auto integer = read< int >( in );
    const auto decimal = read< double >( in );
    const auto word = read< std::string >( in );
    ...
    

    where the variable type is automatically deduced from the return type.

Anything else?

Now, do you know of other alternatives? [In case you wonder, see my replies to comments below for the "why" of this question.]

Thank you.

È stato utile?

Soluzione

Well, you can still use the preprocessor, even though you might not like it for stylistic reasons. You could use it like this:

#define READ(type, variable, stream) type variable = read<type>(stream)

After this, you can write your assignments simply as

READ(int, integer, in);

Of course, this hides the definition of a variable and its initialization behind a function-like construct, which I do not like, but it's a possible solution.

Altri suggerimenti

Here is a way to package the boilerplate up in one spot for every return type deduction function:

// usual sequence boilerplate:
template<unsigned... s> struct seq { typedef seq<s...> type; };
template<unsigned max, unsigned... s > struct make_seq:make_seq<max-1, max-1, s...> {};
template<unsigned... s> struct make_seq<0, s...>:seq<s...> {};

// RTR object, which wraps a functor F to do return type deduction:
template<template<typename>class F>
struct RTR {
  // Stores a `tuple` of arguments, which it forwards to F when cast to anything:
  template<typename... Args>
  struct worker {
    std::tuple<Args...> args;
    worker ( Args&&... a ):args(std::forward<Args>(a)...) {}
    template<typename T, unsigned... s>
    T call(seq<s...>) const {
      return F<T>()( std::forward<Args>(std::get<s>(args))... );
    }
    template<typename T>
    operator T() const
    {
      return call<T>(make_seq<sizeof...(Args)>());
    }
  };
  // Here operator() creates a worker to hold the args and do the actual call to F:
  template<typename... Args>
  worker<Args...> operator()( Args&&... args ) const {
    return {std::forward<Args>(args)...};
  }
};

// We cannot pass function templates around, so instead we require stateless functors:
template<typename T>
struct read {
  T operator()( std::istream& in ) const {
    T x;
    in >> x;
    return x;
  }
};

// and here we introduce reader, a return type deducing wrapper around read:
namespace { RTR<read> reader; }

You'd have to write a lot of return type deducing code for the above boilerplate to be less than the boilerplate for each specific use.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top