Derived Class Defined Later in the Same File "Does Not Exist"

Derived class defined later in the same file does not exist?

Disclaimer: I don't claim to understand the inner workings of Zend. The following is my interpretation of the PHP source, fueled in great part by educated guesses. Even though I am fully confident in the conclusion, the terminology or details might be off. I 'd love to hear from anyone with experience in Zend internals on the matter.

The investigation

From the PHP parser we can see that when a class declaration is encountered the zend_do_early_binding function is called. Here is the code that handles the declaration of derived classes:

case ZEND_DECLARE_INHERITED_CLASS:
{
zend_op *fetch_class_opline = opline-1;
zval *parent_name;
zend_class_entry **pce;

parent_name = &CONSTANT(fetch_class_opline->op2.constant);
if ((zend_lookup_class(Z_STRVAL_P(parent_name), Z_STRLEN_P(parent_name), &pce TSRMLS_CC) == FAILURE) ||
((CG(compiler_options) & ZEND_COMPILE_IGNORE_INTERNAL_CLASSES) &&
((*pce)->type == ZEND_INTERNAL_CLASS))) {
if (CG(compiler_options) & ZEND_COMPILE_DELAYED_BINDING) {
zend_uint *opline_num = &CG(active_op_array)->early_binding;

while (*opline_num != -1) {
opline_num = &CG(active_op_array)->opcodes[*opline_num].result.opline_num;
}
*opline_num = opline - CG(active_op_array)->opcodes;
opline->opcode = ZEND_DECLARE_INHERITED_CLASS_DELAYED;
opline->result_type = IS_UNUSED;
opline->result.opline_num = -1;
}
return;
}
if (do_bind_inherited_class(CG(active_op_array), opline, CG(class_table), *pce, 1 TSRMLS_CC) == NULL) {
return;
}
/* clear unnecessary ZEND_FETCH_CLASS opcode */
zend_del_literal(CG(active_op_array), fetch_class_opline->op2.constant);
MAKE_NOP(fetch_class_opline);

table = CG(class_table);
break;
}

This code immediately calls zend_lookup_class to see if the parent class exists in the symbol table... and then diverges depending on whether the parent is found or not.

Let's first see what it does if the parent class is found:

if (do_bind_inherited_class(CG(active_op_array), opline, CG(class_table), *pce, 1 TSRMLS_CC) == NULL) {
return;
}

Going over to do_bind_inherited_class, we see that the last argument (which in this call is 1) is called compile_time. This sounds interesting. What does it do with this argument?

if (compile_time) {
op1 = &CONSTANT_EX(op_array, opline->op1.constant);
op2 = &CONSTANT_EX(op_array, opline->op2.constant);
} else {
op1 = opline->op1.zv;
op2 = opline->op2.zv;
}

found_ce = zend_hash_quick_find(class_table, Z_STRVAL_P(op1), Z_STRLEN_P(op1), Z_HASH_P(op1), (void **) &pce);

if (found_ce == FAILURE) {
if (!compile_time) {
/* If we're in compile time, in practice, it's quite possible
* that we'll never reach this class declaration at runtime,
* so we shut up about it. This allows the if (!defined('FOO')) { return; }
* approach to work.
*/
zend_error(E_COMPILE_ERROR, "Cannot redeclare class %s", Z_STRVAL_P(op2));
}
return NULL;
} else {
ce = *pce;
}

Okay... so it reads the parent and derived class names either from a static (from the PHP user's perspective) or dynamic context, depending on the compile_time status. It then tries to find the class entry ("ce") in the class table, and if it is not found then... it returns without doing anything in compile time, but emits a fatal error at runtime.

This sounds enormously important. Let's go back to zend_do_early_binding. What does it do if the parent class is not found?

if (CG(compiler_options) & ZEND_COMPILE_DELAYED_BINDING) {
zend_uint *opline_num = &CG(active_op_array)->early_binding;

while (*opline_num != -1) {
opline_num = &CG(active_op_array)->opcodes[*opline_num].result.opline_num;
}
*opline_num = opline - CG(active_op_array)->opcodes;
opline->opcode = ZEND_DECLARE_INHERITED_CLASS_DELAYED;
opline->result_type = IS_UNUSED;
opline->result.opline_num = -1;
}
return;

It seems that it is generating opcodes that will trigger a call to do_bind_inherited_class again -- but this time, the value of compile_time will be 0 (false).

Finally, what about the implementation of the class_exists PHP function? Looking at the source shows this snippet:

found = zend_hash_find(EG(class_table), name, len+1, (void **) &ce);

Great! This class_table variable is the same class_table that gets involved in the do_bind_inherited_class call we saw earlier! So the return value of class_exists depends on whether an entry for the class has already been inserted into class_table by do_bind_inherited_class.

The conclusions

The Zend compiler does not act on include directives at compile time (even if the filename is hardcoded).

If it did, then there would be no reason to emit a class redeclaration fatal error based on the compile_time flag not being set; the error could be emitted unconditionally.

When the compiler encounters a derived class declaration where the base class has not been declared in the same script file, it pushes the act of registering the class in its internal data structures to runtime.

This is evident from the last code snippet above, which sets up a ZEND_DECLARE_INHERITED_CLASS_DELAYED opcode to register the class when the script is executed. At that point the compile_time flag will be false and behavior will be subtly different.

The return value of class_exists depends on whether the class has already been registered.

Since this happens in different ways at compile time and at run time, the behavior of class_exists is also different:

  • classes whose ancestors are all included in the same source file are registered at compile time; they exist and can be instantiated at any point in that script
  • classes which have an ancestor defined in another source file are registered at runtime; before the VM executes the opcodes that correspond to the class definition in the source these classes do not exist for all practical purposes (class_exists returns false, instantiating gives a fatal error)

Class not found in the same file

PHP classes should be defined before instantiation, see the "new" section of the PHP OO documentation.

An easy way to achieve that is by first declaring the classes and then the main code:

--PublicacionController.php--
<?php
/*Some random includes, those are
right as far as I'm concerned*/

class PublicacionController extends Controller{/* STUFF*/}

//AJAX call
if(!empty($_POST)){
if($_POST['call']=='nuevaPublicacion'){
$pc = new PublicacionController();
$pc->nuevaPublicacion($_POST);
exit;
}
}

?>

Constructing base class and inherited class from the same file

I would recommend using a delegating constructor.

 bar(string file_name) : bar(read_bar(file_name)) {} // Delegate to the next constructor.

bar(pair<int, int> in) : foo(in.first), _b(in.second) {}

PS

Unless you have reason to make read_bar a non-static member function, I would recommend making it a static member function. It seems to me that that function would not need any instance specific logic.

Base must be a previously defined class or struct

This is a typical circular dependency. The Base class doesn't need the full definition of Derived, only know that it exists. This is done through forward declarations.

With a proper forward declaration, the base.h header file doesn't need to include the derived.h header file:

//File: base.h
#ifndef _base_h_
#define _base_h_

// Do not include header file for Derived
// Only do forward declaration
class Derived;

class Base {
public:

void start();
void waitToComplete();
virtual ~Base();

static void sleep(Time timeToSleep);

protected:

friend class Derived;
Base ();
virtual void run() {}
};

#endif

On a couple of unrelated notes:

  • The Base::run function should probably be a pure virtual function:

    virtual void run() = 0;
  • Should the Derived class really override the start function?

  • If you're overriding functions in a derived class, you should use the override special modifier identifier:

    void run() override;

Why `this` can't access the derived class members from base class methods when called for derived class object

Me: When the compiler compiles Base::get function, it cannot see Derived::put function.

You : Isn't the Derived::put in the same file? Why can't the compiler see that?

Me: What if there is a Derived1::putttttt defined by somebody 4 years later deriving from Base in another file?

You: mm, maybe I understand.

Check to see if derived class defines a specific instance variable, throw error from metaclass if not

"Normal" instance variables, such as the ones taught since Python 2 early days can't be checked at class creation time - all instance variables are dynamically created when the __init__ (or other) method is executed.

However, since Python 3.6 it is possible to "annotate" variables in the class body - these usually serve only as a hint for static type checking tools, which, in turn, do nothing when the program is actually run.

However, when annotating an attribute in the class body, without providing an initial value (which would then create it as a "class attribute"), it will show in the namespace inside the __annotations__ key (and not as a key themselves).

In short: you can design a metaclass requiring an attribute to be annotated in the class body, although you can't ensure it is actually filled-in with a value inside __init__ before it is actually run. (But it can be checked after it is called the first time - check the second part of this answer).

All in all - you'd need something like this:

class BaseMeta(type):
def __new__(cls, name, bases, namespace):
print(cls, name, bases, namespace)
if name != 'Base' and (
'__annotations__' not in namespace or
'bar' not in namespace['__annotations__']
):
raise TypeError("bar not annotated in derived class body")
return super().__new__(cls, name, bases, namespace)

class Base(metaclass=BaseMeta):
def foo(self):
return self.bar

class Derived(Base):
bar: int
def __init__(self):
self.path = '/path/to/locality'
self.bar = 0

If bar: int is not present in the derived class body, the metaclass will raise. However, if self.bar = 0 is not present inside __init__, the metaclass has no way to "know" it - not without running the code.

Close things present in the language

There has been for sometime in Python "abstractclasses" - they
do almost exactly what your first example proposes: one can mandate
that derived classes implement methods with a certain name. However,
this check is made when the class is first instantiated, not when
it is created. (Thus allowing more than one level of abstract classes inheriting one from another, and that works as far as none of those
is instantiated):


In [68]: from abc import ABC, abstractmethod

In [69]: class Base(ABC):
...: def foo(self):
...: ...
...: @abstractmethod
...: def bar(self): pass
...:

In [70]: class D1(Base): pass

In [71]: D1()
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-71-1689c9d98c94> in <module>
----> 1 D1()

TypeError: Can't instantiate abstract class D1 with abstract methods bar

In [72]: class D2(Base):
...: def bar(self):
...: ...
...:

In [73]: D2()
Out[73]: <__main__.D2 at 0x7ff64270a850>

And then, along with "abstractmethods", the ABC bases (which are implemented with a metaclass not unlike the one in your example, although they do have some support in the language core), it is possible to declare "abstractproperties" - these are declared as class attributes, and will raise an error on class instantiation (just like above), if the derived class do not override the attribute. The main difference to the "annotations" approach above is that this actually requires a value to be set on the attribute on the class body, where as the bar: int declaration do not create an actual class attribute:

In [75]: import abc                                                                                           

In [76]: class Base(ABC):
...: def foo(self):
...: ...
...: bar = abc.abstractproperty()
...:
...:
...:

In [77]: class D1(Base): pass

In [78]: D1()
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-78-1689c9d98c94> in <module>
----> 1 D1()

TypeError: Can't instantiate abstract class D1 with abstract methods bar

In [79]: class D2(Base):
...: bar = 0
...:

In [80]: D2()

I undestand this may not be desirable - but I called the attention to the natural "instantiation time" error-raising, in these cases, because it is possible to do a..

#... Check instance attribute after __init__ run for the first time.

In this approach, the check is only performed when the class is instantiated, not when it is declared - and consists in wrapping __init__ in a decorator that will check for the required attributes after it is run for the first time:

from functools import wraps

class BaseMeta(type):
def __init__(cls, name, bases, namespace):
# Overides __init__ instead of __new__:
# we process "cls" after it was created.
wrapped = cls.__init__
sentinel = object()
@wraps(wrapped)
def _init_wrapper(self, *args, **kw):
wrapped(self, *args, **kw)
errored = []
for attr in cls._required:
if getattr(self, attr, sentinel) is sentinel:
errored.append(attr)
if errored:
raise TypeError(f"Class {cls.__name__} did not set attribute{'s' if len(errored) > 1 else ''} {errored} when instantiated")
# optionally "unwraps" __init__ after the first instance is created:
cls.__init__ = wrapped
if cls.__name__ != "Base":
cls.__init__ = _init_wrapper
super().__init__(name, bases, namespace)

And checking that in the interactive mode:

In [84]: class Base(metaclass=BaseMeta): 
...: _required = ["bar"]
...: def __init__(self):
...: pass
...:

In [85]: class Derived(Base):
...: def __init__(self):
...: pass
...:

In [86]: Derived()
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-87-8da841e1a3d5> in <module>
----> 1 Derived()

<ipython-input-83-8bf317642bf5> in _init_wrapper(self, *args, **kw)
13 errored.append(attr)
14 if errored:
---> 15 raise TypeError(f"Class {cls.__name__} did not set attribute{'s' if len(errored) > 1 else ''} {errored} when instantiated")
16 # optionally "unwraps" __init__ after the first instance is created:
17 cls.__init__ = wrapped

TypeError: Class Derived did not set attribute ['bar'] when instantiated

In [87]: class D2(Base):
...: def __init__(self):
...: self.bar = 0
...:

In [88]: D2()
Out[88]: <__main__.D2 at 0x7ff6418e9a10>

Classes with same file name in different libs inheritance issue

I just made a solution in Microsoft Visual C++ 2010 Express containing two static library projects (lib1 and lib2) and an application project (main) with the contents that you describe and was able to reproduce your problem. Here is the complete build output I get:


1>------ Build started: Project: lib1, Configuration: Debug Win32 ------
1> MyClass.cpp
1> lib1.vcxproj -> c:\users\samuel windwer\documents\visual studio 2010\Projects\linker_test\Debug\lib1.lib
2>------ Build started: Project: lib2, Configuration: Debug Win32 ------
2> MyClass.cpp
2> Replacing Debug\MyClass.obj
2> lib2.vcxproj -> c:\users\samuel windwer\documents\visual studio 2010\Projects\linker_test\Debug\lib2.lib
3>------ Build started: Project: main, Configuration: Debug Win32 ------
3> main.cpp
3>lib2.lib(MyClass.obj) : error LNK2019: unresolved external symbol "public: __thiscall LIB1::MyClass::MyClass(void)" (??0MyClass@LIB1@@QAE@XZ) referenced in function "public: __thiscall LIB2::MyClass::MyClass(void)" (??0MyClass@LIB2@@QAE@XZ)
3>lib2.lib(MyClass.obj) : error LNK2019: unresolved external symbol "public: __thiscall LIB1::MyClass::~MyClass(void)" (??1MyClass@LIB1@@QAE@XZ) referenced in function "public: __thiscall LIB2::MyClass::~MyClass(void)" (??1MyClass@LIB2@@QAE@XZ)
3>c:\users\samuel windwer\documents\visual studio 2010\Projects\linker_test\Debug\main.exe : fatal error LNK1120: 2 unresolved externals
========== Build: 2 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========

The message "Replacing Debug\MyClass.obj" sheds some light on the issue. This message was emitted by Microsoft's Library Manager (LIB.EXE) while building lib2.lib. In order to understand this message, it is important to understand exactly what a static library file (.lib) is, and by extension, exactly what LIB.EXE does.

First thing's first: no code is actually linked when building a static library. A .lib file is just an archive that contains one or more .obj files inside of it. In other words, the purpose of a .lib file is to provide a convenient way to distribute a collection of .obj files as one file. All that LIB.EXE does is package up the project's .obj files into a .lib file. LIB.EXE also has options for doing other stuff with .lib files, such as listing all of the contained .obj files and extracting them; see MSDN for more info.

Your question states that "lib2 links in lib1." I'm assuming this means that you put lib1.lib as an "Additional Dependency" in lib2's project settings, as shown in this screenshot:

Sample Image

In order to understand exactly what adding a .lib file as an "Additional Dependency" of a static library project like this does, after changing this setting, I followed the procedure in this answer to see the command-line that is used when running LIB.EXE to build lib2.lib. Here is is:

lib.exe "/OUT:c:\users\samuel windwer\documents\visual studio 2010\Projects\linker_test\Debug\lib2.lib" lib1.lib /LIBPATH:..\Debug Debug\MyClass.obj

This command produces a new static library file named lib2.lib that contains all of the .obj files in lib1.lib, as well as MyClass.obj. The fact that lib1.lib also contains an object named MyClass.obj is the source of your problem. As is stated in LIB.EXE's documentation on MSDN:

To replace a library member with a new object, specify the library containing the member object to be replaced and the file name for the new object (or the library that contains it). When an object that has the same name exists in more than one input file, LIB puts the last object specified in the LIB command into the output library.

The message "Replacing Debug\MyClass.obj" is printed to the console when LIB.EXE sees the second instance of MyClass.obj because it thinks that the second MyClass.obj is supposed to replace the first one. Renaming one of the MyClass.cpp files to MyClass1.cpp fixes this problem because there are no longer two object files named MyClass.obj, so both of them can live happily together in the same .lib file.

It seems that you've already come up with some workarounds for this, but hopefully you now understand the behavior that you're seeing.



Related Topics



Leave a reply



Submit