PaperBirdMaster
PaperBirdMaster

Reputation: 13320

Adding an operator overloading in specialized template

Wondering about useful uses of the operator , I'm trying to create a little set of helper objects to make easier the construction of DB queries from the C++ code. My idea is to take advantage of the operator , in order of create instructions that resembles the DB calls. The helper objects are the following:

class Fields
{
public:
    Fields &operator ,(const std::string &s)
    {
        SQL.append(s).append(1, ',');
        return *this;
    }

    Fields &operator ,(const Fields &f)
    {
        std::string Result = f;
        SQL.append(Result);
        return *this;
    }

    virtual operator std::string() const = 0;
protected:
    std::string SQL;
};

template <const char *INSTRUCTION> struct Instruction : public Fields
{
    operator std::string() const
    {
        std::string Result(INSTRUCTION);
        return Result.append(SQL);
    }
};

Then, with the correct typedefs and values, this approach allows to do the following:

extern const char SQL_SELECT[] = "SELECT ";
extern const char SQL_FROM[] = "FROM ";
extern const char SQL_WHERE[] = "WHERE ";
extern const char SQL_ORDER_BY[] = "ORDER BY ";

typedef Instruction<SQL_SELECT> SELECT;
typedef Instruction<SQL_FROM> FROM;
typedef Instruction<SQL_WHERE> WHERE;
typedef Instruction<SQL_ORDER_BY> ORDER_BY;

std::string Query = ((SELECT(), "a", "b", "c"),
                     (FROM(), "A", "B"),
                     (WHERE(), "a = b AND c <> b"),
                     (ORDER_BY(), "a", "c"));

std::cout << Query;

Which produces this output: SELECT a,b,c,FROM A,B,WHERE a = b AND c <> b,ORDER_BY a,c, (I'm taking care of the trailing commas in my version, this part is omitted in order to shorten the example), here is the code.

The problem is the instruction ORDER BY. This instruction can take a final operand that changes the behaviour of the ordering, I want to pass (through operator ,) an enum value to the struct Instruction instance:

enum ORDER
{
    ASC,
    DESC,
};

std::string OrderBy = (ORDER_BY(), "a", "c", DESC); // <---- Note the 'DESC' value.

But only want to enable this operator for the Instruction<SQL_ORDER_BY> instances, so I've tried to specialize the template:

template <> struct Instruction<SQL_ORDER_BY> : public Fields
{
    Instruction() : order(ASC) {}

    Fields &operator ,(const ORDER o)
    {
        order = o;
        return *this;
    }

    operator std::string() const
    {
        std::string Result(SQL_ORDER_BY);
        Result.append(SQL);
        Result.append(order == ASC? "ASC": "DESC");
        return Result;
    }

private:
    ORDER order;
};

AFAIK this specialization must have three operator , overloads:

But after creating the specialization, the Query string:

std::string Query = ((SELECT(), "a", "b", "c"),
                     (FROM(), "A", "B"),
                     (WHERE(), "a = b AND c <> b"),
                     (ORDER_BY(), "a", "c"));

Ends having the value: SELECT a,b,c,FROM A,B,WHERE a = b AND c <> b,c,. This is like if ORDER_BY is ignored, and adding the DESC value results in a compilation error:

std::string Query = ((SELECT(), "a", "b", "c"),
                     (FROM(), "A", "B"),
                     (WHERE(), "a = b AND c <> b"),
                     (ORDER_BY(), "a", "c", DESC)); // <-- cannot convert 'ORDER' to 'string'

It seems that ORDER values aren't entering in the operator , of the specialization, but adding a free operator on the same namespace fixes the compilation error:

std::string operator ,(const std::string &left, const ORDER right)
{
    std::string Result(left);
    return Result.append(1, ',').append(right == ASC? "ASC": "DESC");
}

But I really thought that Fields &Instruction<SQL_ORDER_BY>::operator ,(const ORDER) would be called, so I'm now asking for some advice:

  1. Why the Instruction<SQL_ORDER_BY> instances aren't appended to the Query string after specialize the template?
  2. Why the ORDER values aren't calling the Fields &operator ,(const ORDER) provided by the specialization.
  3. How many operator , have the Instruction<SQL_ORDER_BY> instances?

PS: All this effort is for autodidact purposes, almost zero lines of this code will end up in production code, so please avoid comments about using libraries or about the utility of the code,

thanks.

EDIT:

Someone who deleted his answer adviced to add using Fields::operator std::string; and using Fields::operator,; lines to the specialization, doing it fixed the ignoring ORDER_BY problem.

Upvotes: 3

Views: 306

Answers (1)

Andy Prowl
Andy Prowl

Reputation: 126432

The problem is due to the fact that your overload of the , operator in the Instruction<SQL_ORDER_BY> subclass of Fields is hiding the overloaded operators from the superclass. This is just the way function call resolution works in C++: name lookup happens first and stops as soon as a set of names in a certain namespace is found; then, overload resolution is performed.

The problem is explained in this related article by Herb Sutter. The article is not entirely relevant to your problem, but contains the solution to it. In particular, check out "Example 2a".

You have to use using directives to import the Field base class's operator overloads into the scope of the derived classes, so your overload of , in Instruction<SQL_ORDER_BY> won't hide them.

Take this little program as a simple example:

#include <iostream>
#include <string>

using namespace std;

struct A // Class A contains two overloads of operator ,
{
    void operator , (int) { cout << "A::operator , (int)" << endl; }
    void operator , (string) { cout << "A::operator , (string)" << endl; }
};

struct B : A // Class B contains only *one* overload of operator ,
             // Overloads coming from `A` are *hidden* by this one
{
    void operator , (double) { cout << "B::operator , (double)" << endl; }
};

int main()
{
    A a;
    a, 1; // "A::operator , (int)" will be printed to std out
    a, "hello"; // "A::operator , (string)" will be printed to std out

    B b;
    b, 3.0; // "B::operator , (double)" will be printed to the std out
    b, "hello"; // Nothing in the standard output!
}

However, if you change the definition of B this way:

struct B : A
{
    using A::operator ,; // <-- Brings A's overloads into scope!
    void operator , (double) { cout << "B::operator , (double)" << endl; }
};

You will see that the last line of main() in the sample program above will print this to the standard output:

A::operator , (string)

This means that the B's overload of the , operator no more hides the overloads defined in A, which is most likely what you want.

UPDATE:

There is another problem that the answer did not cover yet. The overload of the , operator in your base class Fields returns a reference to an object of type Fields. Since the , operator associates to the left, the expression e1, e2, e3 is evaluated as (e1, e2), e3. In your specific case, the result of (e1, e2) is a reference to a base class that does not support the overload of the , operator which is supported by the derived class.

Let's reduce it once again to a simpler example that mirrors your design:

#include <iostream>
#include <string>

using namespace std;

struct A
{   
    // Operator overloads return a reference to A
    A& operator , (int) 
    { cout << "A::operator , (int)" << endl; return *this; }

    A& operator , (string) 
    { cout << "A::operator , (string)" << endl; *this; }
};

struct B : A
{   
    // Imported overloads still return a reference to A
    using A::operator ,;

    // This overload returns a reference to B 
    B& operator , (double) 
    { cout << "B::operator , (double)" << endl; return *this; }
};

int main()
{
    B b;
    b, 3.0;
    b, "hello", 3.2; // What will be displayed here?
}

Consider the last line of the example. You probably expect it to invoke B::operator , (double), but this is what is printed to the standard output:

A::operator , (int)

Why? Well, because of the associativity of the comma operator and the return type of your overloads. First, the expression b, "hello" is evaluated, and that returns a reference to A. Then, on the result of this expression, the function A::operator , (3.2) will be invoked. A has a viable function, which is the one accepting an int. And that one gets selected. B's overload is not seen because the result of the first expression b, "hello" is of type A&.

So how to solve it? You can use a design pattern called CRTP ("Curiously Recurring Template Pattern") and turn your definitions of A and B into the following:

template<typename T>
struct A
{
    T& operator , (int) 
    { cout << "A::operator , (int)" << endl; return *(static_cast<T*>(this)); }

    T& operator , (string) 
    { cout << "A::operator , (string)" << endl; *(static_cast<T*>(this)); }
};

struct B : A<B>
{
    using A::operator ,;
    B& operator , (double) 
    { cout << "B::operator , (double)" << endl; return *this; }
};

This way, the last line of the main() function in the example above will print what you expect to the standard output:

A::operator , (string)
B::operator , (double)

Upvotes: 1

Related Questions