开发者

c++ template class; function with arbitrary container type, how to define it?

Okay, simple template question. Say I define my template class something like this:

template<typename T>
class foo {
public:
    foo(T const& first, T const& second) : first(first), second(second) {}

    template<typename C>
    void bar(C& container, T const& baz) {
        //...
    }
private:
    T first;
    T second;
}

The question is about my bar function... I need it to be able to use a standard container of some sort, which is why I included the template/typename C part, to define that container type. But apparently that's not the right way to do it, since my test class then complains that:

error: 'bar' was not declared in this scope

So how would I go about implementing my bar function the proper way? That is, as a function of my template class, with an arbitrary container type... the rest of my template class works fine (has other functions that don't result in an error), it's just that one function that's problematic.

EDIT: Okay, so the specific function (bar) is an eraseInRange function, that erases all elements in a specified range:

void eraseInRange(C& container, T const& firstElement, T const& secondElement) {...}

And an example of how it would be used would be:

eraseInRange(v, 开发者_JAVA技巧7, 19);

where v is a vector in this case.

EDIT 2: Silly me! I was supposed to declare the function outside of my class, not in it... pretty frustrating mistake to be making. Anyways, thanks everyone for the help, though the problem was a little different, the information did help me construct the function, since after finding my original problem, I did get some other pleasant errors. So thank you!



Traits solution.

Generalize not more than needed, and not less.

In some cases that solution might not be enough as it will match any template with such signature (e.g. shared_ptr), in which case you could make use of type_traits, very much like duck-typing (templates are duck typed in general).

#include <type_traits>

// Helper to determine whether there's a const_iterator for T.
template<typename T>
struct has_const_iterator
{
private:
    template<typename C> static char test(typename C::const_iterator*);
    template<typename C> static int  test(...);
public:
    enum { value = sizeof(test<T>(0)) == sizeof(char) };
};


// bar() is defined for Containers that define const_iterator as well
// as value_type.
template <typename Container>
typename std::enable_if<has_const_iterator<Container>::value,
                        void>::type
bar(const Container &c, typename Container::value_type const & t)
{
  // Note: no extra check needed for value_type, the check comes for
  //       free in the function signature already.
}


template <typename T>
class DoesNotHaveConstIterator {};

#include <vector>
int main () {
    std::vector<float> c;
    bar (c, 1.2f);

    DoesNotHaveConstIterator<float> b;
    bar (b, 1.2f); // correctly fails to compile
}

A good template usually does not artificially restrict the kind of types for which they are valid (why should they?). But imagine in the example above you need to have access to an objects const_iterator, then you can use SFINAE and type_traits to put those constraints on your function.


Or just to as the standard library does

Generalize not more than needed, and not less.

template <typename Iter>
void bar (Iter it, Iter end) {
    for (; it!=end; ++it) { /*...*/ }
}

#include <vector>
int main () {
    std::vector<float> c;
    bar (c.begin(), c.end());
}

For more such examples, look into <algorithm>.

This approach's strength is its simplicity and is based on concepts like ForwardIterator. It will even work for arrays. If you want to report errors right in the signature, you can combine it with traits.


std containers with signature like std::vector (not recommended)

The simplest solution is approximated by Kerrek SB already, though it is invalid C++. The corrected variant goes like so:

#include <memory> // for std::allocator
template <template <typename, typename> class Container, 
          typename Value,
          typename Allocator=std::allocator<Value> >
void bar(const Container<Value, Allocator> & c, const Value & t)
{
  //
}

However: this will only work for containers that have exactly two template type arguments, so will fail miserably for std::map (thanks Luc Danton).


Any kind of secondary template arguments (not recommended)

The corrected version for any secondary parameter count is as follows:

#include <memory> // for std::allocator<>

template <template <typename, typename...> class Container, 
          typename Value,
          typename... AddParams >
void bar(const Container<Value, AddParams...> & c, const Value & t)
{
  //
}

template <typename T>
class OneParameterVector {};

#include <vector>
int main () {
    OneParameterVector<float> b;
    bar (b, 1.2f);
    std::vector<float> c;
    bar (c, 1.2f);
}

However: this will still fail for non-template containers (thanks Luc Danton).


Make the template templated on a template template parameter:

template <template <typename, typename...> class Container>
void bar(const Container<T> & c, const T & t)
{
  //
}

If you don't have C++11, then you can't use variadic templates, and you have to provide as many template parameters as your container takes. For example, for a sequence container you might need two:

template <template <typename, typename> class Container, typename Alloc>
void bar(const Container<T, Alloc> & c, const T & t);

Or, if you only want to allow allocators which are themselves template instances:

template <template <typename, typename> class Container, template <typename> class Alloc>
void bar(const Container<T, Alloc<T> > & c, const T & t);

As I suggested in the comments, I would personally prefer to make the entire container a templated type and use traits to check if it's valid. Something like this:

template <typename Container>
typename std::enable_if<std::is_same<typename Container::value_type, T>::value, void>::type
bar(const Container & c, const T & t);

This is more flexible since the container can now be anything that exposes the value_type member type. More sophisticated traits for checking for member functions and iterators can be conceived of; for example, the pretty printer implements a few of those.


C++20 solution with Concepts and Ranges

In C++20, with the addition of Concepts and Ranges library, we can solve this simply with std::ranges::common_range:

void printContainer(const std::ranges::common_range auto & container);
{
    for(const auto& item : container) std::cout << item;
}

Here, common_range is a concept that all stl containers satisfy. And you can get container's value type with:

std::ranges::range_value_t<decltype(container)>

You can also create your own container type that satisfy the concept with well defined iterator type and it begin() and it end() functions.

  • Alternatively, you can also use std::ranges::range, which has a bit less strict requirement than common_range, so it could allow more custom types.

Attempting to call the function with a non-satisfying type would give you error like template argument deduction/substitution failed: constraints not satisfied.


Here's the latest and expanded version of this answer and significant improvement over answer by Sabastian.

The idea is to define all traits of STL containers. Unfortunately, this gets tricky very fast and fortunately lot of people have worked on tuning this code. These traits are reusable so just copy and past below code in file called type_utils.hpp (feel free to change these names):

//put this in type_utils.hpp 
#ifndef commn_utils_type_utils_hpp
#define commn_utils_type_utils_hpp

#include <type_traits>
#include <valarray>

namespace common_utils { namespace type_utils {
    //from: https://raw.githubusercontent.com/louisdx/cxx-prettyprint/master/prettyprint.hpp
    //also see https://gist.github.com/louisdx/1076849
    namespace detail
    {
        // SFINAE type trait to detect whether T::const_iterator exists.

        struct sfinae_base
        {
            using yes = char;
            using no  = yes[2];
        };

        template <typename T>
        struct has_const_iterator : private sfinae_base
        {
        private:
            template <typename C> static yes & test(typename C::const_iterator*);
            template <typename C> static no  & test(...);
        public:
            static const bool value = sizeof(test<T>(nullptr)) == sizeof(yes);
            using type =  T;

            void dummy(); //for GCC to supress -Wctor-dtor-privacy
        };

        template <typename T>
        struct has_begin_end : private sfinae_base
        {
        private:
            template <typename C>
            static yes & f(typename std::enable_if<
                std::is_same<decltype(static_cast<typename C::const_iterator(C::*)() const>(&C::begin)),
                             typename C::const_iterator(C::*)() const>::value>::type *);

            template <typename C> static no & f(...);

            template <typename C>
            static yes & g(typename std::enable_if<
                std::is_same<decltype(static_cast<typename C::const_iterator(C::*)() const>(&C::end)),
                             typename C::const_iterator(C::*)() const>::value, void>::type*);

            template <typename C> static no & g(...);

        public:
            static bool const beg_value = sizeof(f<T>(nullptr)) == sizeof(yes);
            static bool const end_value = sizeof(g<T>(nullptr)) == sizeof(yes);

            void dummy(); //for GCC to supress -Wctor-dtor-privacy
        };

    }  // namespace detail

    // Basic is_container template; specialize to derive from std::true_type for all desired container types

    template <typename T>
    struct is_container : public std::integral_constant<bool,
                                                        detail::has_const_iterator<T>::value &&
                                                        detail::has_begin_end<T>::beg_value  &&
                                                        detail::has_begin_end<T>::end_value> { };

    template <typename T, std::size_t N>
    struct is_container<T[N]> : std::true_type { };

    template <std::size_t N>
    struct is_container<char[N]> : std::false_type { };

    template <typename T>
    struct is_container<std::valarray<T>> : std::true_type { };

    template <typename T1, typename T2>
    struct is_container<std::pair<T1, T2>> : std::true_type { };

    template <typename ...Args>
    struct is_container<std::tuple<Args...>> : std::true_type { };

}}  //namespace
#endif

Now you can use these traits to make sure our code only accepts container types. For example, you can implement append function that appends one vector to another like this:

#include "type_utils.hpp"

template<typename Container>
static typename std::enable_if<type_utils::is_container<Container>::value, void>::type
append(Container& to, const Container& from)
{
    using std::begin;
    using std::end;
    to.insert(end(to), begin(from), end(from));
}

Notice that I'm using begin() and end() from std namespace just to be sure we have iterator behavior. For more explanation see my blog post.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜